antoinette 0.1.0 → 0.1.1

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.
@@ -0,0 +1,364 @@
1
+ module Sankey exposing
2
+ ( Diagram
3
+ , Edge
4
+ , Model
5
+ , Node
6
+ , RenderEvents
7
+ , defaults
8
+ , layout
9
+ , render
10
+ )
11
+
12
+ import Dict exposing (Dict)
13
+ import Svg exposing (Svg, g, path, rect, svg, text, text_)
14
+ import Svg.Attributes
15
+ exposing
16
+ ( cursor
17
+ , d
18
+ , fill
19
+ , fillOpacity
20
+ , fontFamily
21
+ , fontSize
22
+ , height
23
+ , rx
24
+ , stroke
25
+ , strokeOpacity
26
+ , strokeWidth
27
+ , textAnchor
28
+ , viewBox
29
+ , width
30
+ , x
31
+ , y
32
+ )
33
+ import Svg.Events exposing (onClick, onMouseOut, onMouseOver)
34
+
35
+
36
+ type alias Model =
37
+ { svgWidth : Float
38
+ , svgHeight : Float
39
+ , columnWidths : Dict Int Float
40
+ , nodePadding : Float
41
+ , columnSpacing : Float
42
+ , edgeColor : String
43
+ , edgeOpacity : Float
44
+ , fontSize : Float
45
+ , highlightedNodeIds : List String
46
+ , lockedNodeIds : List String
47
+ }
48
+
49
+
50
+ defaults : Model
51
+ defaults =
52
+ { svgWidth = 1100
53
+ , svgHeight = 800
54
+ , columnWidths = Dict.empty
55
+ , nodePadding = 10
56
+ , columnSpacing = 300
57
+ , edgeColor = "#999"
58
+ , edgeOpacity = 0.4
59
+ , fontSize = 12
60
+ , highlightedNodeIds = []
61
+ , lockedNodeIds = []
62
+ }
63
+
64
+
65
+ type alias Node =
66
+ { id : String
67
+ , label : String
68
+ , x : Float
69
+ , y : Float
70
+ , height : Float
71
+ }
72
+
73
+
74
+ type alias Edge =
75
+ { fromId : String
76
+ , toId : String
77
+ , fromX : Float
78
+ , fromY : Float
79
+ , toX : Float
80
+ , toY : Float
81
+ , thickness : Float
82
+ }
83
+
84
+
85
+ type alias Diagram =
86
+ { columns : Dict Int (List Node)
87
+ , edges : List Edge
88
+ }
89
+
90
+
91
+ type alias InputNode =
92
+ { id : String
93
+ , label : String
94
+ , column : Int
95
+ }
96
+
97
+
98
+ type alias InputEdge =
99
+ { fromId : String
100
+ , toId : String
101
+ }
102
+
103
+
104
+ layout : Model -> List InputNode -> List InputEdge -> Diagram
105
+ layout model inputNodes inputEdges =
106
+ let
107
+ inputNodesByColumn =
108
+ [ List.filter (\n -> n.column == 0) inputNodes
109
+ , List.filter (\n -> n.column == 1) inputNodes
110
+ , List.filter (\n -> n.column == 2) inputNodes
111
+ ]
112
+
113
+ nodeHeight =
114
+ model.fontSize + 8
115
+
116
+ columnWidth colIndex =
117
+ Dict.get colIndex model.columnWidths |> Maybe.withDefault 100
118
+
119
+ width0 =
120
+ columnWidth 0
121
+
122
+ width1 =
123
+ columnWidth 1
124
+
125
+ width2 =
126
+ columnWidth 2
127
+
128
+ gap =
129
+ model.columnSpacing - (width0 + width1) / 2
130
+
131
+ columnX col =
132
+ case col of
133
+ 0 ->
134
+ 0
135
+
136
+ 1 ->
137
+ width0 + gap
138
+
139
+ _ ->
140
+ width0 + gap + width1 + gap
141
+
142
+ breathingRoom =
143
+ 5
144
+
145
+ positionColumn : Int -> List InputNode -> List Node
146
+ positionColumn colIndex colNodes =
147
+ List.indexedMap
148
+ (\rowIndex inputNode ->
149
+ { id = inputNode.id
150
+ , label = inputNode.label
151
+ , x = columnX colIndex
152
+ , y = breathingRoom + (toFloat rowIndex * (nodeHeight + model.nodePadding))
153
+ , height = nodeHeight
154
+ }
155
+ )
156
+ colNodes
157
+
158
+ columns =
159
+ inputNodesByColumn
160
+ |> List.indexedMap positionColumn
161
+ |> List.indexedMap Tuple.pair
162
+ |> Dict.fromList
163
+
164
+ allNodes =
165
+ Dict.values columns |> List.concat
166
+
167
+ nodeDict =
168
+ List.map (\n -> ( n.id, n )) allNodes
169
+
170
+ findNode nodeId =
171
+ List.head (List.filter (\( id, _ ) -> id == nodeId) nodeDict)
172
+ |> Maybe.map Tuple.second
173
+
174
+ nodeToColumn =
175
+ inputNodes
176
+ |> List.map (\n -> ( n.id, n.column ))
177
+ |> Dict.fromList
178
+
179
+ edgeThickness =
180
+ 4
181
+
182
+ positionEdge : InputEdge -> Maybe Edge
183
+ positionEdge inputEdge =
184
+ Maybe.map2
185
+ (\fromNode toNode ->
186
+ let
187
+ fromColIndex =
188
+ Dict.get inputEdge.fromId nodeToColumn |> Maybe.withDefault 0
189
+
190
+ fromWidth =
191
+ columnWidth fromColIndex
192
+ in
193
+ { fromId = inputEdge.fromId
194
+ , toId = inputEdge.toId
195
+ , fromX = fromNode.x + fromWidth
196
+ , fromY = fromNode.y + (fromNode.height / 2)
197
+ , toX = toNode.x
198
+ , toY = toNode.y + (toNode.height / 2)
199
+ , thickness = edgeThickness
200
+ }
201
+ )
202
+ (findNode inputEdge.fromId)
203
+ (findNode inputEdge.toId)
204
+
205
+ positionedEdges =
206
+ List.filterMap positionEdge inputEdges
207
+ in
208
+ { columns = columns
209
+ , edges = positionedEdges
210
+ }
211
+
212
+
213
+ type alias RenderEvents msg =
214
+ { onSelectNode : String -> msg
215
+ , onDeselectNode : msg
216
+ , onLockNode : String -> msg
217
+ }
218
+
219
+
220
+ render : Model -> RenderEvents msg -> Diagram -> Svg msg
221
+ render model events diagram =
222
+ let
223
+ renderColumn colIndex nodes =
224
+ let
225
+ colWidth =
226
+ Dict.get colIndex model.columnWidths |> Maybe.withDefault 100
227
+ in
228
+ List.map (renderNode model events colWidth) nodes
229
+
230
+ renderedNodes =
231
+ diagram.columns
232
+ |> Dict.toList
233
+ |> List.concatMap (\( colIndex, nodes ) -> renderColumn colIndex nodes)
234
+ in
235
+ svg
236
+ [ width (String.fromFloat model.svgWidth)
237
+ , height (String.fromFloat model.svgHeight)
238
+ , viewBox
239
+ ("0 0 "
240
+ ++ String.fromFloat model.svgWidth
241
+ ++ " "
242
+ ++ String.fromFloat model.svgHeight
243
+ )
244
+ ]
245
+ [ g [] (List.map (renderEdge model) diagram.edges)
246
+ , g [] renderedNodes
247
+ ]
248
+
249
+
250
+ renderNode : Model -> RenderEvents msg -> Float -> Node -> Svg msg
251
+ renderNode model events nodeWidth node =
252
+ let
253
+ thisNodeLocked =
254
+ List.member node.id model.lockedNodeIds
255
+
256
+ anyNodesLocked =
257
+ not (List.isEmpty model.lockedNodeIds)
258
+
259
+ interactionEvents =
260
+ if thisNodeLocked then
261
+ [ onClick (events.onLockNode node.id)
262
+ , cursor "pointer"
263
+ ]
264
+
265
+ else if anyNodesLocked then
266
+ []
267
+
268
+ else
269
+ [ onClick (events.onLockNode node.id)
270
+ , onMouseOver (events.onSelectNode node.id)
271
+ , onMouseOut events.onDeselectNode
272
+ , cursor "pointer"
273
+ ]
274
+
275
+ nodeAttrs =
276
+ interactionEvents
277
+
278
+ isHighlighted =
279
+ List.member node.id model.highlightedNodeIds
280
+
281
+ borderColor =
282
+ if isHighlighted then
283
+ "hotpink"
284
+
285
+ else
286
+ "#60A5FA"
287
+ in
288
+ g nodeAttrs
289
+ [ rect
290
+ [ x (String.fromFloat node.x)
291
+ , y (String.fromFloat node.y)
292
+ , width (String.fromFloat nodeWidth)
293
+ , height (String.fromFloat node.height)
294
+ , fill "white"
295
+ , fillOpacity "0.5"
296
+ , stroke borderColor
297
+ , strokeWidth "1"
298
+ , rx "3"
299
+ ]
300
+ []
301
+ , text_
302
+ [ x (String.fromFloat (node.x + 5))
303
+ , y (String.fromFloat (node.y + node.height / 2 + 4))
304
+ , fontSize (String.fromFloat model.fontSize)
305
+ , fontFamily "system-ui, sans-serif"
306
+ , textAnchor "start"
307
+ , fill "#374151"
308
+ ]
309
+ [ text node.label ]
310
+ ]
311
+
312
+
313
+ renderEdge : Model -> Edge -> Svg msg
314
+ renderEdge model edge =
315
+ let
316
+ midX =
317
+ (edge.fromX + edge.toX) / 2
318
+
319
+ controlX1 =
320
+ edge.fromX + (midX - edge.fromX) * 0.5
321
+
322
+ controlX2 =
323
+ edge.toX - (edge.toX - midX) * 0.5
324
+
325
+ pathD =
326
+ String.join " "
327
+ [ "M"
328
+ , String.fromFloat edge.fromX
329
+ , String.fromFloat edge.fromY
330
+ , "C"
331
+ , String.fromFloat controlX1
332
+ , String.fromFloat edge.fromY
333
+ , String.fromFloat controlX2
334
+ , String.fromFloat edge.toY
335
+ , String.fromFloat edge.toX
336
+ , String.fromFloat edge.toY
337
+ ]
338
+
339
+ isHighlighted =
340
+ List.member edge.fromId model.highlightedNodeIds
341
+ || List.member edge.toId model.highlightedNodeIds
342
+
343
+ strokeColor =
344
+ if isHighlighted then
345
+ "hotpink"
346
+
347
+ else
348
+ model.edgeColor
349
+
350
+ opacity =
351
+ if isHighlighted then
352
+ 1.0
353
+
354
+ else
355
+ model.edgeOpacity
356
+ in
357
+ path
358
+ [ d pathD
359
+ , stroke strokeColor
360
+ , strokeWidth (String.fromFloat edge.thickness)
361
+ , strokeOpacity (String.fromFloat opacity)
362
+ , fill "none"
363
+ ]
364
+ []
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Antoinette
4
+ class GraphController < ::ApplicationController
5
+ def show
6
+ @bundle_graph = File.read(Rails.root.join("config/antoinette.json"))
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,11 @@
1
+ <div id="bundle-graph"></div>
2
+
3
+ <script>
4
+ document.addEventListener('DOMContentLoaded', function() {
5
+ var container = document.getElementById('bundle-graph');
6
+ Elm.BundleGraph.init({
7
+ node: container,
8
+ flags: <%= raw @bundle_graph %>
9
+ });
10
+ });
11
+ </script>
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :antoinette do
4
+ desc "Build JavaScript bundles from Elm apps"
5
+ task build: :environment do
6
+ Antoinette::CLI::Commands::Build.new.call
7
+ end
8
+ end
9
+
10
+ Rake::Task["assets:precompile"].enhance(["antoinette:build"])
metadata CHANGED
@@ -1,31 +1,126 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: antoinette
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Giles Bowkett
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2026-01-05 00:00:00.000000000 Z
12
- dependencies: []
13
- description:
14
- email:
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rails
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '7.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '7.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: csv
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '3.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '3.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: dry-cli
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '1.0'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '1.0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: haikunator
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '1.1'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '1.1'
68
+ - !ruby/object:Gem::Dependency
69
+ name: uglifier
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '4.2'
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '4.2'
82
+ description: Antoinette analyzes which Elm apps are used in Rails views, bundles them
83
+ together, and injects script tags into templates. Minimizes HTTP requests while
84
+ ensuring each page only loads the Elm apps it needs.
85
+ email:
86
+ - gilesb@gmail.com
15
87
  executables: []
16
88
  extensions: []
17
89
  extra_rdoc_files: []
18
90
  files:
19
- - CODE_OF_CONDUCT.md
91
+ - CHANGELOG.md
20
92
  - LICENSE.txt
21
93
  - README.md
22
94
  - lib/antoinette.rb
95
+ - lib/antoinette/cli/commands.rb
96
+ - lib/antoinette/engine.rb
97
+ - lib/antoinette/services/clear_script_tag.rb
98
+ - lib/antoinette/services/compile_elm.rb
99
+ - lib/antoinette/services/concat_bundle.rb
100
+ - lib/antoinette/services/elm_app_usage_analyzer.rb
101
+ - lib/antoinette/services/inject_script_tag.rb
102
+ - lib/antoinette/services/partial_resolver.rb
103
+ - lib/antoinette/services/weaver.rb
23
104
  - lib/antoinette/version.rb
24
- homepage:
105
+ - lib/generators/antoinette/install_generator.rb
106
+ - lib/generators/antoinette/templates/BundleGraph.elm
107
+ - lib/generators/antoinette/templates/Sankey.elm
108
+ - lib/generators/antoinette/templates/graph_controller.rb
109
+ - lib/generators/antoinette/templates/show.html.erb
110
+ - lib/tasks/antoinette.rake
111
+ homepage: https://github.com/gilesbowkett/antoinette
25
112
  licenses:
26
113
  - MIT
27
- metadata: {}
28
- post_install_message:
114
+ metadata:
115
+ changelog_uri: https://github.com/gilesbowkett/antoinette/blob/main/CHANGELOG.md
116
+ post_install_message: |2+
117
+
118
+ Thanks for installing Antoinette!
119
+
120
+ To complete setup, run:
121
+
122
+ bin/rails generate antoinette:install
123
+
29
124
  rdoc_options: []
30
125
  require_paths:
31
126
  - lib
@@ -33,15 +128,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
33
128
  requirements:
34
129
  - - ">="
35
130
  - !ruby/object:Gem::Version
36
- version: '0'
131
+ version: 3.1.0
37
132
  required_rubygems_version: !ruby/object:Gem::Requirement
38
133
  requirements:
39
134
  - - ">="
40
135
  - !ruby/object:Gem::Version
41
136
  version: '0'
42
137
  requirements: []
43
- rubygems_version: 3.0.3.1
44
- signing_key:
138
+ rubygems_version: 4.0.3
45
139
  specification_version: 4
46
- summary: Weave Elm
140
+ summary: Weaves Elm apps into JavaScript bundles for Rails templates
47
141
  test_files: []
142
+ ...
data/CODE_OF_CONDUCT.md DELETED
@@ -1,74 +0,0 @@
1
- # Contributor Covenant Code of Conduct
2
-
3
- ## Our Pledge
4
-
5
- In the interest of fostering an open and welcoming environment, we as
6
- contributors and maintainers pledge to making participation in our project and
7
- our community a harassment-free experience for everyone, regardless of age, body
8
- size, disability, ethnicity, gender identity and expression, level of experience,
9
- nationality, personal appearance, race, religion, or sexual identity and
10
- orientation.
11
-
12
- ## Our Standards
13
-
14
- Examples of behavior that contributes to creating a positive environment
15
- include:
16
-
17
- * Using welcoming and inclusive language
18
- * Being respectful of differing viewpoints and experiences
19
- * Gracefully accepting constructive criticism
20
- * Focusing on what is best for the community
21
- * Showing empathy towards other community members
22
-
23
- Examples of unacceptable behavior by participants include:
24
-
25
- * The use of sexualized language or imagery and unwelcome sexual attention or
26
- advances
27
- * Trolling, insulting/derogatory comments, and personal or political attacks
28
- * Public or private harassment
29
- * Publishing others' private information, such as a physical or electronic
30
- address, without explicit permission
31
- * Other conduct which could reasonably be considered inappropriate in a
32
- professional setting
33
-
34
- ## Our Responsibilities
35
-
36
- Project maintainers are responsible for clarifying the standards of acceptable
37
- behavior and are expected to take appropriate and fair corrective action in
38
- response to any instances of unacceptable behavior.
39
-
40
- Project maintainers have the right and responsibility to remove, edit, or
41
- reject comments, commits, code, wiki edits, issues, and other contributions
42
- that are not aligned to this Code of Conduct, or to ban temporarily or
43
- permanently any contributor for other behaviors that they deem inappropriate,
44
- threatening, offensive, or harmful.
45
-
46
- ## Scope
47
-
48
- This Code of Conduct applies both within project spaces and in public spaces
49
- when an individual is representing the project or its community. Examples of
50
- representing a project or community include using an official project e-mail
51
- address, posting via an official social media account, or acting as an appointed
52
- representative at an online or offline event. Representation of a project may be
53
- further defined and clarified by project maintainers.
54
-
55
- ## Enforcement
56
-
57
- Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
- reported by contacting the project team at gilesb@gmail.com. All
59
- complaints will be reviewed and investigated and will result in a response that
60
- is deemed necessary and appropriate to the circumstances. The project team is
61
- obligated to maintain confidentiality with regard to the reporter of an incident.
62
- Further details of specific enforcement policies may be posted separately.
63
-
64
- Project maintainers who do not follow or enforce the Code of Conduct in good
65
- faith may face temporary or permanent repercussions as determined by other
66
- members of the project's leadership.
67
-
68
- ## Attribution
69
-
70
- This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
- available at [http://contributor-covenant.org/version/1/4][version]
72
-
73
- [homepage]: http://contributor-covenant.org
74
- [version]: http://contributor-covenant.org/version/1/4/