twitter-bootstrap-rails 5.3.0 → 5.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +81 -0
  3. data/README.md +128 -628
  4. data/app/assets/javascripts/twitter/bootstrap/bootstrap.min.js +3 -3
  5. data/app/assets/stylesheets/twitter-bootstrap-static/bootstrap.css +3 -3
  6. data/lib/generators/bootstrap/badge/badge_generator.rb +13 -0
  7. data/lib/generators/bootstrap/breadcrumb/breadcrumb_generator.rb +13 -0
  8. data/lib/generators/bootstrap/button/button_generator.rb +13 -0
  9. data/lib/generators/bootstrap/cdn/cdn_generator.rb +39 -0
  10. data/lib/generators/bootstrap/cdn_tags.rb +43 -0
  11. data/lib/generators/bootstrap/dropdown/dropdown_generator.rb +13 -0
  12. data/lib/generators/bootstrap/features/features_generator.rb +13 -0
  13. data/lib/generators/bootstrap/footer/footer_generator.rb +13 -0
  14. data/lib/generators/bootstrap/header/header_generator.rb +13 -0
  15. data/lib/generators/bootstrap/hero/hero_generator.rb +13 -0
  16. data/lib/generators/bootstrap/install/install_generator.rb +120 -61
  17. data/lib/generators/bootstrap/install/templates/application.js +1 -1
  18. data/lib/generators/bootstrap/install/templates/bootstrap.js +10 -3
  19. data/lib/generators/bootstrap/install/templates/bootstrap_and_overrides.css +2 -2
  20. data/lib/generators/bootstrap/jumbotron/jumbotron_generator.rb +13 -0
  21. data/lib/generators/bootstrap/layout/layout_generator.rb +65 -1
  22. data/lib/generators/bootstrap/layout/templates/layout.html.erb +14 -9
  23. data/lib/generators/bootstrap/layout/templates/layout.html.haml +28 -43
  24. data/lib/generators/bootstrap/layout/templates/layout.html.slim +25 -40
  25. data/lib/generators/bootstrap/list_group/list_group_generator.rb +13 -0
  26. data/lib/generators/bootstrap/modal/modal_generator.rb +13 -0
  27. data/lib/generators/bootstrap/sidebar/sidebar_generator.rb +13 -0
  28. data/lib/generators/bootstrap/snippets/snippet_base.rb +126 -0
  29. data/lib/generators/bootstrap/snippets/snippet_catalog.rb +134 -0
  30. data/lib/generators/bootstrap/snippets/templates/badges/pills.html.erb +10 -0
  31. data/lib/generators/bootstrap/snippets/templates/badges/subtle.html.erb +10 -0
  32. data/lib/generators/bootstrap/snippets/templates/badges/subtle_bordered.html.erb +20 -0
  33. data/lib/generators/bootstrap/snippets/templates/badges/with_avatar.html.erb +34 -0
  34. data/lib/generators/bootstrap/snippets/templates/badges/with_avatar_divider.html.erb +66 -0
  35. data/lib/generators/bootstrap/snippets/templates/badges/with_icons.html.erb +26 -0
  36. data/lib/generators/bootstrap/snippets/templates/breadcrumbs/basic.html.erb +9 -0
  37. data/lib/generators/bootstrap/snippets/templates/breadcrumbs/chevron.html.erb +18 -0
  38. data/lib/generators/bootstrap/snippets/templates/breadcrumbs/custom.html.erb +18 -0
  39. data/lib/generators/bootstrap/snippets/templates/breadcrumbs/with_icons.html.erb +18 -0
  40. data/lib/generators/bootstrap/snippets/templates/buttons/circle.html.erb +20 -0
  41. data/lib/generators/bootstrap/snippets/templates/buttons/grid.html.erb +6 -0
  42. data/lib/generators/bootstrap/snippets/templates/buttons/loading.html.erb +10 -0
  43. data/lib/generators/bootstrap/snippets/templates/buttons/pills.html.erb +11 -0
  44. data/lib/generators/bootstrap/snippets/templates/buttons/with_icons.html.erb +14 -0
  45. data/lib/generators/bootstrap/snippets/templates/dropdowns/calendar.html.erb +156 -0
  46. data/lib/generators/bootstrap/snippets/templates/dropdowns/mega_menu.html.erb +140 -0
  47. data/lib/generators/bootstrap/snippets/templates/dropdowns/simple.html.erb +20 -0
  48. data/lib/generators/bootstrap/snippets/templates/dropdowns/with_icons.html.erb +88 -0
  49. data/lib/generators/bootstrap/snippets/templates/dropdowns/with_search.html.erb +64 -0
  50. data/lib/generators/bootstrap/snippets/templates/features/columns_with_icons.html.erb +46 -0
  51. data/lib/generators/bootstrap/snippets/templates/features/custom_cards.html.erb +79 -0
  52. data/lib/generators/bootstrap/snippets/templates/features/hanging_icons.html.erb +41 -0
  53. data/lib/generators/bootstrap/snippets/templates/features/icon_grid.html.erb +77 -0
  54. data/lib/generators/bootstrap/snippets/templates/features/with_title.html.erb +50 -0
  55. data/lib/generators/bootstrap/snippets/templates/footers/columns.html.erb +43 -0
  56. data/lib/generators/bootstrap/snippets/templates/footers/simple.html.erb +15 -0
  57. data/lib/generators/bootstrap/snippets/templates/footers/with_brand.html.erb +22 -0
  58. data/lib/generators/bootstrap/snippets/templates/footers/with_nav.html.erb +12 -0
  59. data/lib/generators/bootstrap/snippets/templates/footers/with_newsletter.html.erb +62 -0
  60. data/lib/generators/bootstrap/snippets/templates/headers/centered.html.erb +15 -0
  61. data/lib/generators/bootstrap/snippets/templates/headers/dark_double.html.erb +66 -0
  62. data/lib/generators/bootstrap/snippets/templates/headers/dark_search.html.erb +25 -0
  63. data/lib/generators/bootstrap/snippets/templates/headers/double.html.erb +28 -0
  64. data/lib/generators/bootstrap/snippets/templates/headers/grid_dropdown.html.erb +47 -0
  65. data/lib/generators/bootstrap/snippets/templates/headers/light_search.html.erb +34 -0
  66. data/lib/generators/bootstrap/snippets/templates/headers/nav_pills.html.erb +11 -0
  67. data/lib/generators/bootstrap/snippets/templates/headers/with_auth_buttons.html.erb +22 -0
  68. data/lib/generators/bootstrap/snippets/templates/heroes/centered.html.erb +13 -0
  69. data/lib/generators/bootstrap/snippets/templates/heroes/centered_screenshot.html.erb +17 -0
  70. data/lib/generators/bootstrap/snippets/templates/heroes/cropped_image.html.erb +17 -0
  71. data/lib/generators/bootstrap/snippets/templates/heroes/dark.html.erb +14 -0
  72. data/lib/generators/bootstrap/snippets/templates/heroes/signup_form.html.erb +26 -0
  73. data/lib/generators/bootstrap/snippets/templates/heroes/with_image.html.erb +17 -0
  74. data/lib/generators/bootstrap/snippets/templates/icons/_bootstrap_icons.html.erb +134 -0
  75. data/lib/generators/bootstrap/snippets/templates/jumbotrons/basic.html.erb +10 -0
  76. data/lib/generators/bootstrap/snippets/templates/jumbotrons/full_width.html.erb +12 -0
  77. data/lib/generators/bootstrap/snippets/templates/jumbotrons/placeholder.html.erb +13 -0
  78. data/lib/generators/bootstrap/snippets/templates/jumbotrons/with_icon.html.erb +22 -0
  79. data/lib/generators/bootstrap/snippets/templates/list-groups/checkable.html.erb +24 -0
  80. data/lib/generators/bootstrap/snippets/templates/list-groups/checkboxes.html.erb +45 -0
  81. data/lib/generators/bootstrap/snippets/templates/list-groups/checkboxes_expanded.html.erb +52 -0
  82. data/lib/generators/bootstrap/snippets/templates/list-groups/radios.html.erb +32 -0
  83. data/lib/generators/bootstrap/snippets/templates/list-groups/with_avatars.html.erb +36 -0
  84. data/lib/generators/bootstrap/snippets/templates/modals/confirm.html.erb +18 -0
  85. data/lib/generators/bootstrap/snippets/templates/modals/sheet.html.erb +19 -0
  86. data/lib/generators/bootstrap/snippets/templates/modals/signup.html.erb +44 -0
  87. data/lib/generators/bootstrap/snippets/templates/modals/whats_new.html.erb +41 -0
  88. data/lib/generators/bootstrap/snippets/templates/sidebars/collapsible.html.erb +90 -0
  89. data/lib/generators/bootstrap/snippets/templates/sidebars/dark.html.erb +67 -0
  90. data/lib/generators/bootstrap/snippets/templates/sidebars/icon_only.html.erb +59 -0
  91. data/lib/generators/bootstrap/snippets/templates/sidebars/light.html.erb +67 -0
  92. data/lib/generators/bootstrap/snippets/templates/sidebars/list_group.html.erb +94 -0
  93. data/lib/generators/bootstrap/starter/starter_generator.rb +37 -0
  94. data/lib/generators/bootstrap/starter/templates/starter.html.erb +13 -0
  95. data/lib/twitter/bootstrap/rails/bootstrap.rb +1 -0
  96. data/lib/twitter/bootstrap/rails/config.rb +54 -0
  97. data/lib/twitter/bootstrap/rails/engine.rb +0 -8
  98. data/lib/twitter/bootstrap/rails/version.rb +28 -1
  99. data/lib/twitter-bootstrap-rails.rb +0 -1
  100. data/spec/lib/generators/cdn_generator_spec.rb +42 -0
  101. data/spec/lib/generators/install_generator_spec.rb +148 -0
  102. data/spec/lib/generators/layout_generator_spec.rb +124 -0
  103. data/spec/lib/generators/no_hardcoded_versions_spec.rb +45 -0
  104. data/spec/lib/generators/snippet_generators_spec.rb +201 -0
  105. data/spec/lib/generators/starter_generator_spec.rb +54 -0
  106. data/spec/spec_helper.rb +5 -0
  107. data/spec/support/generator_spec_helper.rb +87 -0
  108. data/vendor/assets/javascripts/twitter/bootstrap/bootstrap.bundle.min.js +3 -3
  109. data/vendor/assets/stylesheets/twitter/bootstrap/bootstrap.min.css +3 -3
  110. metadata +99 -123
  111. data/app/assets/fonts/glyphicons-halflings-regular.eot +0 -0
  112. data/app/assets/fonts/glyphicons-halflings-regular.svg +0 -288
  113. data/app/assets/fonts/glyphicons-halflings-regular.ttf +0 -0
  114. data/app/assets/fonts/glyphicons-halflings-regular.woff +0 -0
  115. data/app/assets/fonts/glyphicons-halflings-regular.woff2 +0 -0
  116. data/app/helpers/glyph_helper.rb +0 -23
  117. data/lib/generators/bootstrap/install/templates/bootstrap.coffee +0 -3
  118. data/lib/generators/bootstrap/install/templates/bootstrap_and_overrides.less +0 -21
  119. data/spec/lib/twitter_bootstrap_rails/glyph_helper_spec.rb +0 -24
  120. data/vendor/assets/stylesheets/twitter-bootstrap-static/bootstrap.css.erb +0 -6684
  121. data/vendor/static-source/bootstrap.less +0 -1
  122. data/vendor/static-source/sprites.less +0 -6
  123. data/vendor/toolkit/twitter/bootstrap/alerts.less +0 -73
  124. data/vendor/toolkit/twitter/bootstrap/badges.less +0 -66
  125. data/vendor/toolkit/twitter/bootstrap/bootstrap.less +0 -55
  126. data/vendor/toolkit/twitter/bootstrap/breadcrumbs.less +0 -26
  127. data/vendor/toolkit/twitter/bootstrap/button-groups.less +0 -244
  128. data/vendor/toolkit/twitter/bootstrap/buttons.less +0 -166
  129. data/vendor/toolkit/twitter/bootstrap/carousel.less +0 -270
  130. data/vendor/toolkit/twitter/bootstrap/close.less +0 -34
  131. data/vendor/toolkit/twitter/bootstrap/code.less +0 -69
  132. data/vendor/toolkit/twitter/bootstrap/component-animations.less +0 -33
  133. data/vendor/toolkit/twitter/bootstrap/dropdowns.less +0 -216
  134. data/vendor/toolkit/twitter/bootstrap/forms.less +0 -613
  135. data/vendor/toolkit/twitter/bootstrap/glyphicons.less +0 -305
  136. data/vendor/toolkit/twitter/bootstrap/grid.less +0 -84
  137. data/vendor/toolkit/twitter/bootstrap/input-groups.less +0 -171
  138. data/vendor/toolkit/twitter/bootstrap/jumbotron.less +0 -54
  139. data/vendor/toolkit/twitter/bootstrap/labels.less +0 -64
  140. data/vendor/toolkit/twitter/bootstrap/list-group.less +0 -130
  141. data/vendor/toolkit/twitter/bootstrap/media.less +0 -66
  142. data/vendor/toolkit/twitter/bootstrap/mixins/alerts.less +0 -14
  143. data/vendor/toolkit/twitter/bootstrap/mixins/background-variant.less +0 -9
  144. data/vendor/toolkit/twitter/bootstrap/mixins/border-radius.less +0 -18
  145. data/vendor/toolkit/twitter/bootstrap/mixins/buttons.less +0 -65
  146. data/vendor/toolkit/twitter/bootstrap/mixins/center-block.less +0 -7
  147. data/vendor/toolkit/twitter/bootstrap/mixins/clearfix.less +0 -22
  148. data/vendor/toolkit/twitter/bootstrap/mixins/forms.less +0 -85
  149. data/vendor/toolkit/twitter/bootstrap/mixins/gradients.less +0 -59
  150. data/vendor/toolkit/twitter/bootstrap/mixins/grid-framework.less +0 -91
  151. data/vendor/toolkit/twitter/bootstrap/mixins/grid.less +0 -122
  152. data/vendor/toolkit/twitter/bootstrap/mixins/hide-text.less +0 -21
  153. data/vendor/toolkit/twitter/bootstrap/mixins/image.less +0 -33
  154. data/vendor/toolkit/twitter/bootstrap/mixins/labels.less +0 -12
  155. data/vendor/toolkit/twitter/bootstrap/mixins/list-group.less +0 -30
  156. data/vendor/toolkit/twitter/bootstrap/mixins/nav-divider.less +0 -10
  157. data/vendor/toolkit/twitter/bootstrap/mixins/nav-vertical-align.less +0 -9
  158. data/vendor/toolkit/twitter/bootstrap/mixins/opacity.less +0 -8
  159. data/vendor/toolkit/twitter/bootstrap/mixins/pagination.less +0 -24
  160. data/vendor/toolkit/twitter/bootstrap/mixins/panels.less +0 -24
  161. data/vendor/toolkit/twitter/bootstrap/mixins/progress-bar.less +0 -10
  162. data/vendor/toolkit/twitter/bootstrap/mixins/reset-filter.less +0 -8
  163. data/vendor/toolkit/twitter/bootstrap/mixins/reset-text.less +0 -18
  164. data/vendor/toolkit/twitter/bootstrap/mixins/resize.less +0 -6
  165. data/vendor/toolkit/twitter/bootstrap/mixins/responsive-visibility.less +0 -15
  166. data/vendor/toolkit/twitter/bootstrap/mixins/size.less +0 -10
  167. data/vendor/toolkit/twitter/bootstrap/mixins/tab-focus.less +0 -9
  168. data/vendor/toolkit/twitter/bootstrap/mixins/table-row.less +0 -28
  169. data/vendor/toolkit/twitter/bootstrap/mixins/text-emphasis.less +0 -9
  170. data/vendor/toolkit/twitter/bootstrap/mixins/text-overflow.less +0 -8
  171. data/vendor/toolkit/twitter/bootstrap/mixins/vendor-prefixes.less +0 -227
  172. data/vendor/toolkit/twitter/bootstrap/mixins.less +0 -40
  173. data/vendor/toolkit/twitter/bootstrap/modals.less +0 -150
  174. data/vendor/toolkit/twitter/bootstrap/navbar.less +0 -660
  175. data/vendor/toolkit/twitter/bootstrap/navs.less +0 -242
  176. data/vendor/toolkit/twitter/bootstrap/normalize.less +0 -424
  177. data/vendor/toolkit/twitter/bootstrap/pager.less +0 -54
  178. data/vendor/toolkit/twitter/bootstrap/pagination.less +0 -89
  179. data/vendor/toolkit/twitter/bootstrap/panels.less +0 -271
  180. data/vendor/toolkit/twitter/bootstrap/popovers.less +0 -131
  181. data/vendor/toolkit/twitter/bootstrap/print.less +0 -101
  182. data/vendor/toolkit/twitter/bootstrap/progress-bars.less +0 -87
  183. data/vendor/toolkit/twitter/bootstrap/responsive-embed.less +0 -35
  184. data/vendor/toolkit/twitter/bootstrap/responsive-utilities.less +0 -194
  185. data/vendor/toolkit/twitter/bootstrap/scaffolding.less +0 -161
  186. data/vendor/toolkit/twitter/bootstrap/tables.less +0 -234
  187. data/vendor/toolkit/twitter/bootstrap/theme.less +0 -291
  188. data/vendor/toolkit/twitter/bootstrap/thumbnails.less +0 -36
  189. data/vendor/toolkit/twitter/bootstrap/tooltip.less +0 -101
  190. data/vendor/toolkit/twitter/bootstrap/type.less +0 -302
  191. data/vendor/toolkit/twitter/bootstrap/utilities.less +0 -55
  192. data/vendor/toolkit/twitter/bootstrap/variables.less +0 -1751
  193. data/vendor/toolkit/twitter/bootstrap/wells.less +0 -29
  194. data/vendor/toolkit/twitter/bootstrap_base.less +0 -2
@@ -0,0 +1,54 @@
1
+ require "twitter/bootstrap/rails/version"
2
+
3
+ module Twitter
4
+ module Bootstrap
5
+ module Rails
6
+ # How the host app loads Bootstrap's assets. `rails g bootstrap:install cdn`
7
+ # writes an initializer setting this to :cdn; `bootstrap:layout` reads it to
8
+ # pick its default. Defaults to :static so the layout generator works whether
9
+ # or not that initializer exists.
10
+ VALID_ASSET_MODES = [:static, :cdn].freeze
11
+
12
+ @asset_mode = :static
13
+
14
+ class << self
15
+ attr_reader :asset_mode
16
+
17
+ def asset_mode=(mode)
18
+ mode = mode.to_sym
19
+ unless VALID_ASSET_MODES.include?(mode)
20
+ raise ArgumentError,
21
+ "asset_mode must be one of #{VALID_ASSET_MODES.inspect}, got #{mode.inspect}"
22
+ end
23
+ @asset_mode = mode
24
+ end
25
+
26
+ def cdn?
27
+ asset_mode == :cdn
28
+ end
29
+
30
+ # Which asset pipeline the host app runs. Propshaft (the Rails 8 default)
31
+ # has no Sprockets directives: a `//= require` line in a manifest is an
32
+ # inert comment there, so assets have to be linked explicitly instead.
33
+ def asset_pipeline
34
+ return @asset_pipeline if defined?(@asset_pipeline) && @asset_pipeline
35
+
36
+ if defined?(::Propshaft)
37
+ :propshaft
38
+ elsif defined?(::Sprockets)
39
+ :sprockets
40
+ else
41
+ :unknown
42
+ end
43
+ end
44
+
45
+ # Set explicitly to override detection (used by the specs).
46
+ attr_writer :asset_pipeline
47
+
48
+ def propshaft?
49
+ asset_pipeline == :propshaft
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -12,14 +12,6 @@ module Twitter
12
12
  module Bootstrap
13
13
  module Rails
14
14
  class Engine < ::Rails::Engine
15
- initializer 'twitter-bootstrap-rails.setup',
16
- :after => 'less-rails.after.load_config_initializers',
17
- :group => :all do |app|
18
- if defined?(Less)
19
- app.config.less.paths << File.join(config.root, 'vendor', 'toolkit')
20
- end
21
- end
22
-
23
15
  initializer 'twitter-bootstrap-rails.setup_helpers' do |app|
24
16
  app.config.to_prepare do
25
17
  ActionController::Base.send :include, Breadcrumbs
@@ -1,7 +1,34 @@
1
1
  module Twitter
2
2
  module Bootstrap
3
3
  module Rails
4
- VERSION = "5.3.0"
4
+ VERSION = "5.4.0"
5
+
6
+ # The Bootstrap release this gem vendors and links to. The gem version no
7
+ # longer tracks it; this constant is the authoritative statement.
8
+ BOOTSTRAP_VERSION = "5.3.8"
9
+ POPPER_VERSION = "2.11.8"
10
+
11
+ # URLs and Subresource Integrity hashes exactly as published on
12
+ # https://getbootstrap.com/docs/5.3/getting-started/introduction/
13
+ # Bumping Bootstrap means editing this hash and re-vendoring the dist files.
14
+ BOOTSTRAP_CDN = {
15
+ :css => {
16
+ :url => "https://cdn.jsdelivr.net/npm/bootstrap@#{BOOTSTRAP_VERSION}/dist/css/bootstrap.min.css",
17
+ :integrity => "sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB"
18
+ },
19
+ :bundle => {
20
+ :url => "https://cdn.jsdelivr.net/npm/bootstrap@#{BOOTSTRAP_VERSION}/dist/js/bootstrap.bundle.min.js",
21
+ :integrity => "sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI"
22
+ },
23
+ :js => {
24
+ :url => "https://cdn.jsdelivr.net/npm/bootstrap@#{BOOTSTRAP_VERSION}/dist/js/bootstrap.min.js",
25
+ :integrity => "sha384-G/EV+4j2dNv+tEPo3++6LCgdCROaejBqfUeNjuKAiuXbjrxilcCdDz6ZAVfHWe1Y"
26
+ },
27
+ :popper => {
28
+ :url => "https://cdn.jsdelivr.net/npm/@popperjs/core@#{POPPER_VERSION}/dist/umd/popper.min.js",
29
+ :integrity => "sha384-I7E8VVD/ismYTF4hNIPjVp/Zjvgyol6VFvRkX/vR+Vc4jQkC+hVqc2pM8ODewa9r"
30
+ }
31
+ }.freeze
5
32
  end
6
33
  end
7
34
  end
@@ -6,5 +6,4 @@ module Twitter
6
6
  end
7
7
  end
8
8
 
9
- #require 'less-rails'
10
9
  require 'twitter/bootstrap/rails/bootstrap' if defined?(Rails)
@@ -0,0 +1,42 @@
1
+ require 'spec_helper'
2
+
3
+ describe Bootstrap::Generators::CdnGenerator do
4
+ let(:cdn) { Twitter::Bootstrap::Rails::BOOTSTRAP_CDN }
5
+
6
+ it "prints the stylesheet link and the JS bundle by default" do
7
+ output = run_generator(described_class)
8
+
9
+ expect(output).to include(cdn[:css][:url])
10
+ expect(output).to include(cdn[:css][:integrity])
11
+ expect(output).to include(cdn[:bundle][:url])
12
+ expect(output).to include(cdn[:bundle][:integrity])
13
+ end
14
+
15
+ it "prints only the stylesheet link with --css" do
16
+ output = run_generator(described_class, %w[--css])
17
+
18
+ expect(output).to include(cdn[:css][:url])
19
+ expect(output).not_to include(cdn[:bundle][:url])
20
+ end
21
+
22
+ it "prints only the bundle script with --js" do
23
+ output = run_generator(described_class, %w[--js])
24
+
25
+ expect(output).to include(cdn[:bundle][:url])
26
+ expect(output).not_to include(cdn[:css][:url])
27
+ end
28
+
29
+ it "prints the Popper pair with --separate-popper" do
30
+ output = run_generator(described_class, %w[--separate-popper])
31
+
32
+ expect(output).to include(cdn[:popper][:url])
33
+ expect(output).to include(cdn[:js][:url])
34
+ expect(output).not_to include(cdn[:bundle][:url])
35
+ end
36
+
37
+ it "writes nothing to disk" do
38
+ run_generator(described_class)
39
+
40
+ expect(Dir.children(destination_root)).to be_empty
41
+ end
42
+ end
@@ -0,0 +1,148 @@
1
+ require 'spec_helper'
2
+
3
+ describe Bootstrap::Generators::InstallGenerator do
4
+ let(:vendored_css) { 'app/assets/stylesheets/twitter/bootstrap/bootstrap.min.css' }
5
+ let(:vendored_js) { 'app/assets/javascripts/twitter/bootstrap/bootstrap.bundle.min.js' }
6
+
7
+ describe "static (the default)" do
8
+ it "vendors both Bootstrap dist files into the app" do
9
+ run_generator(described_class, %w[static])
10
+
11
+ expect(destination_exist?(vendored_css)).to be true
12
+ expect(destination_exist?(vendored_js)).to be true
13
+ end
14
+
15
+ it "vendors the Bootstrap version this gem declares" do
16
+ run_generator(described_class, %w[static])
17
+
18
+ expect(read_destination(vendored_css)).to include("Bootstrap v#{Twitter::Bootstrap::Rails::BOOTSTRAP_VERSION}")
19
+ expect(read_destination(vendored_js)).to include("Bootstrap v#{Twitter::Bootstrap::Rails::BOOTSTRAP_VERSION}")
20
+ end
21
+
22
+ it "defaults to static when no mode is given" do
23
+ run_generator(described_class)
24
+
25
+ expect(destination_exist?(vendored_css)).to be true
26
+ end
27
+
28
+ it "writes manifests that require the vendored files" do
29
+ run_generator(described_class, %w[static])
30
+
31
+ expect(read_destination('app/assets/javascripts/application.js'))
32
+ .to include("//= require twitter/bootstrap/bootstrap.bundle.min")
33
+ expect(read_destination('app/assets/stylesheets/bootstrap_and_overrides.css'))
34
+ .to include("*= require twitter/bootstrap/bootstrap.min")
35
+ end
36
+
37
+ it "adds the require lines to manifests the app already has" do
38
+ write_destination('app/assets/javascripts/application.js', "// application\n//= require_tree .\n")
39
+ write_destination('app/assets/stylesheets/application.css', "/*\n *= require_self\n *= require_tree .\n*/\n")
40
+
41
+ run_generator(described_class, %w[static])
42
+
43
+ expect(read_destination('app/assets/javascripts/application.js'))
44
+ .to include("//= require twitter/bootstrap/bootstrap.bundle.min")
45
+ expect(read_destination('app/assets/stylesheets/application.css'))
46
+ .to include("*= require bootstrap_and_overrides")
47
+ end
48
+
49
+ it "says what to add by hand when a manifest has no Sprockets anchor" do
50
+ # A Propshaft app's application.css is plain CSS with nothing to anchor to.
51
+ write_destination('app/assets/stylesheets/application.css', "/*\n * Propshaft manifest.\n */\n")
52
+
53
+ output = run_generator(described_class, %w[static])
54
+
55
+ expect(output).to match(/has no `require_self` directive to anchor to/)
56
+ expect(output).to include("*= require bootstrap_and_overrides")
57
+ expect(read_destination('app/assets/stylesheets/application.css')).not_to include("require bootstrap_and_overrides")
58
+ end
59
+
60
+ it "does not duplicate require lines when run twice" do
61
+ run_generator(described_class, %w[static])
62
+ run_generator(described_class, %w[static])
63
+
64
+ js = read_destination('app/assets/javascripts/application.js')
65
+ expect(js.scan("require twitter/bootstrap/bootstrap.bundle.min").length).to eq(1)
66
+ end
67
+
68
+ it "writes no CDN initializer" do
69
+ run_generator(described_class, %w[static])
70
+
71
+ expect(destination_exist?('config/initializers/bootstrap.rb')).to be false
72
+ end
73
+
74
+ it "raises rather than silently installing nothing when a dist file is missing" do
75
+ allow(File).to receive(:file?).and_call_original
76
+ allow(File).to receive(:file?).with(/bootstrap\.min\.css\z/).and_return(false)
77
+
78
+ expect {
79
+ run_generator(described_class, %w[static])
80
+ }.to raise_error(described_class::MissingAssetError, /bootstrap\.min\.css/)
81
+ end
82
+ end
83
+
84
+ # Propshaft is the Rails 8 default and has no Sprockets directives: a
85
+ # `//= require` line there is an inert comment, so writing manifests that rely
86
+ # on one would ship an app with no Bootstrap at all.
87
+ describe "static on a Propshaft app" do
88
+ before { Twitter::Bootstrap::Rails.asset_pipeline = :propshaft }
89
+
90
+ it "still vendors both dist files" do
91
+ run_generator(described_class, %w[static])
92
+
93
+ expect(destination_exist?(vendored_css)).to be true
94
+ expect(destination_exist?(vendored_js)).to be true
95
+ end
96
+
97
+ it "writes no Sprockets manifests" do
98
+ run_generator(described_class, %w[static])
99
+
100
+ expect(destination_exist?('app/assets/javascripts/application.js')).to be false
101
+ expect(destination_exist?('app/assets/stylesheets/bootstrap_and_overrides.css')).to be false
102
+ end
103
+
104
+ it "does not touch an existing application.css" do
105
+ original = "/*\n * Propshaft manifest.\n */\n"
106
+ write_destination('app/assets/stylesheets/application.css', original)
107
+
108
+ run_generator(described_class, %w[static])
109
+
110
+ expect(read_destination('app/assets/stylesheets/application.css')).to eq(original)
111
+ end
112
+
113
+ it "points the user at bootstrap:layout" do
114
+ output = run_generator(described_class, %w[static])
115
+
116
+ expect(output).to match(/vendored for Propshaft/)
117
+ end
118
+ end
119
+
120
+ describe "cdn" do
121
+ it "vendors nothing" do
122
+ run_generator(described_class, %w[cdn])
123
+
124
+ expect(destination_exist?(vendored_css)).to be false
125
+ expect(destination_exist?(vendored_js)).to be false
126
+ end
127
+
128
+ it "writes an initializer selecting cdn mode" do
129
+ run_generator(described_class, %w[cdn])
130
+
131
+ expect(read_destination('config/initializers/bootstrap.rb'))
132
+ .to include("Twitter::Bootstrap::Rails.asset_mode = :cdn")
133
+ end
134
+
135
+ it "creates no asset manifests" do
136
+ run_generator(described_class, %w[cdn])
137
+
138
+ expect(destination_exist?('app/assets/javascripts/application.js')).to be false
139
+ expect(destination_exist?('app/assets/stylesheets/bootstrap_and_overrides.css')).to be false
140
+ end
141
+ end
142
+
143
+ it "rejects an unknown mode" do
144
+ expect {
145
+ run_generator(described_class, %w[less])
146
+ }.to raise_error(Rails::Generators::Error, /Expected 'static' or 'cdn'/)
147
+ end
148
+ end
@@ -0,0 +1,124 @@
1
+ require 'spec_helper'
2
+
3
+ describe Bootstrap::Generators::LayoutGenerator do
4
+ let(:cdn) { Twitter::Bootstrap::Rails::BOOTSTRAP_CDN }
5
+
6
+ # The generated layout for the given engine.
7
+ def generate_layout(args = [], engine: :erb)
8
+ stub_rails_application(engine)
9
+ run_generator(described_class, args)
10
+ read_destination("app/views/layouts/application.html.#{engine}")
11
+ end
12
+
13
+ [:erb, :haml, :slim].each do |engine|
14
+ context "with the #{engine} template engine" do
15
+ it "uses the asset pipeline by default" do
16
+ layout = generate_layout([], :engine => engine)
17
+
18
+ expect(layout).to include('stylesheet_link_tag "application"')
19
+ expect(layout).to include('javascript_include_tag "application"')
20
+ expect(layout).not_to include("cdn.jsdelivr.net")
21
+ end
22
+
23
+ it "emits the CDN link and bundle with --cdn" do
24
+ layout = generate_layout(%w[--cdn], :engine => engine)
25
+
26
+ expect(layout).to include(cdn[:css][:url])
27
+ expect(layout).to include(cdn[:css][:integrity])
28
+ expect(layout).to include(cdn[:bundle][:url])
29
+ expect(layout).to include(cdn[:bundle][:integrity])
30
+ expect(layout).to include('crossorigin="anonymous"')
31
+ end
32
+
33
+ it "does not also link the pipeline when using --cdn" do
34
+ layout = generate_layout(%w[--cdn], :engine => engine)
35
+
36
+ expect(layout).not_to include('stylesheet_link_tag')
37
+ expect(layout).not_to include('javascript_include_tag')
38
+ end
39
+
40
+ it "emits Popper and bootstrap.js instead of the bundle with --cdn --separate-popper" do
41
+ layout = generate_layout(%w[--cdn --separate-popper], :engine => engine)
42
+
43
+ expect(layout).to include(cdn[:popper][:url])
44
+ expect(layout).to include(cdn[:popper][:integrity])
45
+ expect(layout).to include(cdn[:js][:url])
46
+ expect(layout).to include(cdn[:js][:integrity])
47
+ expect(layout).not_to include(cdn[:bundle][:url])
48
+ end
49
+
50
+ it "includes the important globals: doctype, html lang, and viewport" do
51
+ layout = generate_layout([], :engine => engine)
52
+
53
+ expect(layout).to match(/\A(<!doctype html>|!!!|doctype html)/)
54
+ expect(layout).to match(/lang[^\n]*en/)
55
+ expect(layout).to include("width=device-width, initial-scale=1")
56
+ end
57
+
58
+ it "carries no Bootstrap 3 markup" do
59
+ layout = generate_layout([], :engine => engine)
60
+
61
+ expect(layout).not_to include("navbar-default")
62
+ expect(layout).not_to include("icon-bar")
63
+ expect(layout).not_to include("html5shiv")
64
+ expect(layout).not_to match(/\bwell\b/)
65
+ end
66
+ end
67
+ end
68
+
69
+ describe "on a Propshaft app" do
70
+ before { Twitter::Bootstrap::Rails.asset_pipeline = :propshaft }
71
+
72
+ [:erb, :haml, :slim].each do |engine|
73
+ it "links the vendored Bootstrap explicitly in #{engine}, since require directives are inert" do
74
+ layout = generate_layout([], :engine => engine)
75
+
76
+ expect(layout).to include('stylesheet_link_tag "twitter/bootstrap/bootstrap.min", "application"')
77
+ expect(layout).to include('javascript_include_tag "twitter/bootstrap/bootstrap.bundle.min"')
78
+ end
79
+ end
80
+
81
+ it "does not link an application.js that Propshaft apps do not have" do
82
+ layout = generate_layout
83
+
84
+ expect(layout).not_to include('javascript_include_tag "twitter/bootstrap/bootstrap.bundle.min", "application"')
85
+ end
86
+
87
+ it "still uses the CDN when asked" do
88
+ layout = generate_layout(%w[--cdn])
89
+
90
+ expect(layout).to include(cdn[:css][:url])
91
+ expect(layout).not_to include('stylesheet_link_tag')
92
+ end
93
+ end
94
+
95
+ it "names the layout after the given argument" do
96
+ stub_rails_application(:erb)
97
+ run_generator(described_class, %w[admin])
98
+
99
+ expect(destination_exist?("app/views/layouts/admin.html.erb")).to be true
100
+ end
101
+
102
+ it "warns and falls back to the bundle when --separate-popper is used without --cdn" do
103
+ stub_rails_application(:erb)
104
+ output = run_generator(described_class, %w[--separate-popper])
105
+
106
+ expect(output).to match(/separate-popper only applies with --cdn/)
107
+ expect(read_destination("app/views/layouts/application.html.erb")).not_to include(cdn[:popper][:url])
108
+ end
109
+
110
+ it "defaults to CDN tags when bootstrap:install recorded cdn mode" do
111
+ Twitter::Bootstrap::Rails.asset_mode = :cdn
112
+ layout = generate_layout
113
+
114
+ expect(layout).to include(cdn[:css][:url])
115
+ end
116
+
117
+ it "lets --no-cdn override a recorded cdn mode" do
118
+ Twitter::Bootstrap::Rails.asset_mode = :cdn
119
+ layout = generate_layout(%w[--no-cdn])
120
+
121
+ expect(layout).to include('stylesheet_link_tag "application"')
122
+ expect(layout).not_to include("cdn.jsdelivr.net")
123
+ end
124
+ end
@@ -0,0 +1,45 @@
1
+ require 'spec_helper'
2
+
3
+ # Bumping Bootstrap should be a one-file change. If a template hardcodes a CDN
4
+ # URL or an integrity hash, that promise quietly breaks.
5
+ describe "generator templates" do
6
+ gem_root = File.expand_path("../../../..", __FILE__)
7
+
8
+ let(:template_files) do
9
+ Dir[File.join(gem_root, "lib/generators/**/templates/**/*")].select { |f| File.file?(f) }
10
+ end
11
+
12
+ it "has templates to check" do
13
+ expect(template_files).not_to be_empty
14
+ end
15
+
16
+ it "hardcodes no Bootstrap CDN version" do
17
+ offenders = template_files.select { |f| File.read(f).include?("bootstrap@") }
18
+
19
+ expect(offenders).to be_empty,
20
+ "these templates hardcode a CDN version instead of using the constants: #{offenders.inspect}"
21
+ end
22
+
23
+ it "hardcodes no Subresource Integrity hash" do
24
+ offenders = template_files.select { |f| File.read(f).match?(/sha384-/) }
25
+
26
+ expect(offenders).to be_empty,
27
+ "these templates hardcode an integrity hash instead of using the constants: #{offenders.inspect}"
28
+ end
29
+
30
+ it "keeps the vendored assets on the version the gem declares" do
31
+ version = Twitter::Bootstrap::Rails::BOOTSTRAP_VERSION
32
+
33
+ {
34
+ "vendor/assets/stylesheets/twitter/bootstrap/bootstrap.min.css" => "Bootstrap v#{version}",
35
+ "vendor/assets/javascripts/twitter/bootstrap/bootstrap.bundle.min.js" => "Bootstrap v#{version}",
36
+ "app/assets/stylesheets/twitter-bootstrap-static/bootstrap.css" => "Bootstrap v#{version}",
37
+ "app/assets/javascripts/twitter/bootstrap/bootstrap.min.js" => "Bootstrap v#{version}"
38
+ }.each do |path, banner|
39
+ full = File.join(gem_root, path)
40
+ expect(File.exist?(full)).to be(true), "missing vendored asset #{path}"
41
+ expect(File.read(full, 400)).to include(banner),
42
+ "#{path} is not Bootstrap #{version}"
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,201 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Bootstrap snippet generators" do
4
+ Catalog = Bootstrap::Generators::SnippetCatalog
5
+ TEMPLATE_ROOT = File.expand_path("../../../../lib/generators/bootstrap/snippets/templates", __FILE__)
6
+
7
+ def generator_for(name)
8
+ Bootstrap::Generators.const_get("#{name.split('_').map(&:capitalize).join}Generator")
9
+ end
10
+
11
+ describe "the catalog" do
12
+ it "covers all twelve snippet categories from the docs" do
13
+ expect(Catalog::GENERATORS.values.sort).to eq(%w[
14
+ badges breadcrumbs buttons dropdowns features footers headers heroes
15
+ jumbotrons list-groups modals sidebars
16
+ ].sort)
17
+ end
18
+
19
+ it "has a template file for every catalogued variant" do
20
+ missing = Catalog::VARIANTS.flat_map do |category, variants|
21
+ variants.map { |v| "#{category}/#{v[:name]}" }
22
+ .reject { |rel| File.exist?(File.join(TEMPLATE_ROOT, "#{rel}.html.erb")) }
23
+ end
24
+
25
+ expect(missing).to be_empty, "catalogued variants with no template: #{missing.inspect}"
26
+ end
27
+
28
+ it "has a catalog entry for every template file" do
29
+ on_disk = Dir[File.join(TEMPLATE_ROOT, "*", "*.html.erb")]
30
+ .reject { |f| File.basename(File.dirname(f)) == "icons" }
31
+ .map { |f| "#{File.basename(File.dirname(f))}/#{File.basename(f, '.html.erb')}" }
32
+ catalogued = Catalog::VARIANTS.flat_map { |c, vs| vs.map { |v| "#{c}/#{v[:name]}" } }
33
+
34
+ expect((on_disk - catalogued)).to be_empty,
35
+ "templates with no catalog entry: #{(on_disk - catalogued).inspect}"
36
+ end
37
+
38
+ it "flags exactly the variants whose markup references the icon sprite" do
39
+ Catalog::VARIANTS.each do |category, variants|
40
+ variants.each do |v|
41
+ markup = File.read(File.join(TEMPLATE_ROOT, category, "#{v[:name]}.html.erb"))
42
+ expect(markup.include?('xlink:href="#')).to eq(v[:icons]),
43
+ "#{category}/#{v[:name]} :icons flag is #{v[:icons]} but markup says otherwise"
44
+ end
45
+ end
46
+ end
47
+
48
+ it "flags exactly the variants that reference docs-site placeholder images" do
49
+ placeholders = ["bootstrap-docs.png", "bootstrap-themes.png",
50
+ "/docs/5.3/assets/", "unsplash-photo-"]
51
+
52
+ Catalog::VARIANTS.each do |category, variants|
53
+ variants.each do |v|
54
+ markup = File.read(File.join(TEMPLATE_ROOT, category, "#{v[:name]}.html.erb"))
55
+ expect(placeholders.any? { |p| markup.include?(p) }).to eq(!!v[:images]),
56
+ "#{category}/#{v[:name]} :images flag is #{v[:images]} but markup says otherwise"
57
+ end
58
+ end
59
+ end
60
+
61
+ it "gives every variant a unique name within its category" do
62
+ Catalog::VARIANTS.each do |category, variants|
63
+ names = variants.map { |v| v[:name] }
64
+ expect(names.uniq).to eq(names), "duplicate variant names in #{category}"
65
+ end
66
+ end
67
+ end
68
+
69
+ describe "the shipped snippet markup" do
70
+ let(:templates) { Dir[File.join(TEMPLATE_ROOT, "*", "*.html.erb")] }
71
+
72
+ it "carries no leftover docs page chrome" do
73
+ offenders = templates.select do |f|
74
+ body = File.read(f)
75
+ body.include?("b-example-divider") || body.include?("bd-mode-toggle") ||
76
+ body.match?(/<h1 class="visually-hidden">[^<]*examples/)
77
+ end
78
+
79
+ expect(offenders).to be_empty, "page chrome leaked into: #{offenders.inspect}"
80
+ end
81
+
82
+ it "keeps viewBox correctly cased so icons render" do
83
+ offenders = templates.select { |f| File.read(f).include?("viewbox=") }
84
+
85
+ expect(offenders).to be_empty, "lowercased viewBox (icons will not render) in: #{offenders.inspect}"
86
+ end
87
+
88
+ it "references only icons that the sprite defines" do
89
+ sprite = File.read(File.join(TEMPLATE_ROOT, "icons", "_bootstrap_icons.html.erb"))
90
+ defined_ids = sprite.scan(/<symbol id="([^"]+)"/).flatten
91
+
92
+ referenced = templates.flat_map { |f| File.read(f).scan(/<use xlink:href="#([^"]+)"/).flatten }.uniq
93
+
94
+ expect(referenced - defined_ids).to be_empty,
95
+ "snippets reference icons missing from the sprite: #{(referenced - defined_ids).inspect}"
96
+ end
97
+ end
98
+
99
+ describe "generating a snippet" do
100
+ it "writes the default variant as a partial" do
101
+ run_generator(generator_for("header"))
102
+
103
+ expect(destination_exist?('app/views/shared/_header.html.erb')).to be true
104
+ end
105
+
106
+ it "writes the requested variant" do
107
+ run_generator(generator_for("hero"), %w[dark])
108
+
109
+ partial = read_destination('app/views/shared/_hero.html.erb')
110
+ expected = File.read(File.join(TEMPLATE_ROOT, "heroes", "dark.html.erb"))
111
+ expect(partial).to eq(expected)
112
+ end
113
+
114
+ it "accepts dashes in place of underscores" do
115
+ run_generator(generator_for("footer"), %w[with-newsletter])
116
+
117
+ expect(read_destination('app/views/shared/_footer.html.erb'))
118
+ .to eq(File.read(File.join(TEMPLATE_ROOT, "footers", "with_newsletter.html.erb")))
119
+ end
120
+
121
+ it "honours --as" do
122
+ run_generator(generator_for("footer"), %w[columns --as=site_footer])
123
+
124
+ expect(destination_exist?('app/views/shared/_site_footer.html.erb')).to be true
125
+ expect(destination_exist?('app/views/shared/_footer.html.erb')).to be false
126
+ end
127
+
128
+ it "honours --path" do
129
+ run_generator(generator_for("badge"), %w[pills --path=app/views/components])
130
+
131
+ expect(destination_exist?('app/views/components/_badge.html.erb')).to be true
132
+ end
133
+
134
+ it "tells the user how to render it" do
135
+ output = run_generator(generator_for("modal"), %w[confirm])
136
+
137
+ expect(output).to include('render "shared/modal"')
138
+ end
139
+
140
+ it "raises on an unknown variant, listing the valid ones" do
141
+ expect {
142
+ run_generator(generator_for("badge"), %w[neon])
143
+ }.to raise_error(Rails::Generators::Error, /Unknown badges variant "neon".*pills, subtle/m)
144
+ end
145
+ end
146
+
147
+ describe "the icons sprite" do
148
+ it "is installed alongside a snippet that needs it" do
149
+ run_generator(generator_for("header"), %w[dark_search])
150
+
151
+ expect(destination_exist?('app/views/shared/_bootstrap_icons.html.erb')).to be true
152
+ end
153
+
154
+ it "is not installed for a snippet that does not use icons" do
155
+ run_generator(generator_for("header"), %w[nav_pills])
156
+
157
+ expect(destination_exist?('app/views/shared/_bootstrap_icons.html.erb')).to be false
158
+ end
159
+
160
+ it "can be skipped with --no-icons" do
161
+ run_generator(generator_for("header"), %w[dark_search --no-icons])
162
+
163
+ expect(destination_exist?('app/views/shared/_header.html.erb')).to be true
164
+ expect(destination_exist?('app/views/shared/_bootstrap_icons.html.erb')).to be false
165
+ end
166
+
167
+ it "follows --path" do
168
+ run_generator(generator_for("header"), %w[dark_search --path=app/views/components])
169
+
170
+ expect(destination_exist?('app/views/components/_bootstrap_icons.html.erb')).to be true
171
+ end
172
+ end
173
+
174
+ describe "--list" do
175
+ it "prints each variant with its description and writes nothing" do
176
+ output = run_generator(generator_for("sidebar"), %w[--list])
177
+
178
+ expect(output).to include("icon_only")
179
+ expect(output).to include("Narrow icon-only sidebar")
180
+ expect(output).to include("Default: dark")
181
+ expect(Dir.children(destination_root)).to be_empty
182
+ end
183
+ end
184
+
185
+ describe "every generator" do
186
+ Bootstrap::Generators::SnippetCatalog::GENERATORS.each do |generator_name, category|
187
+ it "bootstrap:#{generator_name} generates each of its #{category} variants" do
188
+ klass = generator_for(generator_name)
189
+
190
+ Catalog.variant_names(category).each do |variant|
191
+ remove_destination_root
192
+ run_generator(klass, [variant])
193
+
194
+ partial = destination_path("app/views/shared/_#{generator_name}.html.erb")
195
+ expect(File.exist?(partial)).to be(true), "#{generator_name} #{variant} wrote nothing"
196
+ expect(File.size(partial)).to be > 50
197
+ end
198
+ end
199
+ end
200
+ end
201
+ end