Project

General

Profile

Feature #23980 » svg-icon-fetch-task.patch

Takashi Kato, 2024-07-28 18:36

View differences:

config/icon_config.yml
1
application:
2
  prefix: "icon-"
3
  dir: icons
4
file:
5
  prefix: "icon-file."
6
  dir: icons/file
7
jstoolbar:
8
  prefix: "jstb_"
9
  dir: icons/jstoolbar
10
background:
11
  template: '--icon-%s: %s;'
12
  dir: icons
13
other:
14
  dir: icons
config/icon_source.yml
1
application:
2
  - name: add
3
    iconset: fa6-solid
4
    svg: circle-plus
5
    style:
6
      - add
7
      - add-bullet
8
  - name: edit
9
    iconset: fa6-solid
10
    svg: pencil
11
  - name: copy
12
    iconset: fa6-solid
13
    svg: clone
14
  - name: duplicate
15
    iconset: fa6-solid
16
    svg: file-circle-plus
17
  - name: del
18
    iconset: fa6-regular
19
    svg: trash-can
20
  - name: move
21
    iconset: fa6-solid
22
    svg: share
23
  - name: save
24
    iconset: fa6-regular
25
    svg: floppy-disk
26
  - name: download
27
    iconset: fa6-solid
28
  - name: cancel
29
    iconset: fa6-solid
30
    svg: reply
31
  - name: table-multiple
32
    iconset: fa-solid
33
    svg: sync-alt
34
    style:
35
      - multiple
36
  - name: folder
37
    iconset: fa6-regular
38
  - name: folder-open
39
    iconset: fa6-solid
40
    style:
41
      - .open .icon-folder
42
  - name: package
43
    iconset: fa6-solid
44
    svg: cube
45
  - name: user
46
    iconset: fa6-solid
47
  - name: projects
48
    iconset: fa6-solid
49
    svg: cubes
50
    style:
51
      - projects
52
      - project
53
  - name: help
54
    iconset: fa6-solid
55
    svg: circle-info
56
  - name: attachment
57
    iconset: fa6-solid
58
    svg: paperclip
59
  - name: history
60
    iconset: fa6-solid
61
    svg: clock-rotate-left
62
  - name: time
63
    iconset: fa6-regular
64
    svg: clock
65
    style:
66
      - time
67
      - time-entry
68
      - time-add
69
  - name: stats
70
    iconset: fa6-regular
71
    svg: chart-bar
72
  - name: warning
73
    iconset: fa6-solid
74
    svg: triangle-exclamation
75
  - name: error
76
    iconset: fa6-solid
77
    svg: circle-exclamation
78
  - name: fav
79
    iconset: fa6-solid
80
    svg: star
81
  - name: fav-off
82
    iconset: fa6-regular
83
    svg: star
84
  - name: reload
85
    iconset: fa-solid
86
    svg: sync
87
  - name: lock
88
    iconset: fa6-solid
89
    style:
90
      - lock
91
      - locked
92
  - name: unlock
93
    iconset: fa6-solid
94
  - name: checked
95
    iconset: fa6-solid
96
    svg: check
97
  - name: report
98
    iconset: fluent
99
    svg: document-lightning-32-regular
100
  - name: comment
101
    iconset: fa6-regular
102
    style:
103
      - comment
104
      - comments
105
      - message
106
      - issue-note
107
  - name: summary
108
    iconset: fa6-solid
109
    svg: bolt-lightning
110
  - name: server-authentication
111
    iconset: fa6-solid
112
    svg: key
113
  - name: issue
114
    iconset: fa6-regular
115
    svg: note-sticky
116
  - name: zoom-in
117
    iconset: fa-solid
118
    svg: search-plus
119
  - name: zoom-out
120
    iconset: fa-solid
121
    svg: search-minus
122
  - name: textfield-key
123
    iconset: fa6-solid
124
    svg: key
125
    style:
126
      - passwd
127
  - name: arrow-right
128
    iconset: fa6-solid
129
    style:
130
      - allow-right
131
      - test
132
      - sticky
133
  - name: email
134
    iconset: fa6-regular
135
    svg: envelope
136
    style:
137
      - email
138
      - email-add
139
  - name: email-disabled
140
    iconset: fa6-solid
141
    svg: envelope
142
  - name: "true"
143
    iconset: fa6-solid
144
    svg: check
145
    style:
146
      - ok
147
  - name: "false"
148
    iconset: fa6-solid
149
    svg: xmark
150
    style:
151
      - not-ok
152
  - name: link-break
153
    iconset: fa6-solid
154
    svg: link-slash
155
  - name: text-list-bullets
156
    iconset: fa6-solid
157
    svg: list-ul
158
    style:
159
      - list
160
  - name: close
161
    iconset: fa6-solid
162
    svg: square-xmark
163
  - name: settings
164
    iconset: fa6-solid
165
    svg: gear
166
  - name: group
167
    iconset: fa6-solid
168
    svg: user-group
169
    style:
170
      - group
171
      - groupnonmember
172
      - groupanonymous
173
  - name: roles
174
    iconset: fa6-solid
175
    svg: eye
176
  - name: issue-edit
177
    iconset: fa6-solid
178
    svg: pen-to-square
179
  - name: workflows
180
    iconset: fa6-solid
181
    svg: gears
182
  - name: textfield
183
    iconset: fa6-regular
184
    svg: file-lines
185
    style:
186
      - custom-fields
187
  - name: plugin
188
    iconset: fa6-solid
189
    svg: puzzle-piece
190
    style:
191
      - plugins
192
  - name: news
193
    iconset: fa6-regular
194
    svg: newspaper
195
  - name: issue-closed
196
    iconset: fa6-regular
197
    svg: square-check
198
  - name: changeset
199
    iconset: fa6-solid
200
    svg: code
201
  - name: comments
202
    iconset: fa6-regular
203
    style:
204
      - reply
205
  - name: wiki-edit
206
    iconset: fa6-solid
207
    svg: book
208
    style:
209
      - wiki-page
210
  - name: document
211
    iconset: fa6-regular
212
    svg: file
213
  - name: link
214
    iconset: fa6-solid
215
    style:
216
      - shared
217
  - name: 3-bullets
218
    iconset: fa6-solid
219
    svg: ellipsis
220
    style:
221
      - actions
222
  - name: reorder
223
    iconset: fa6-solid
224
    svg: arrows-up-down
225
    style:
226
      - sort-handle
227
  - name: angle-down
228
    iconset: fa6-solid
229
    style:
230
      - expanded
231
  - name: angle-right
232
    iconset: fa6-solid
233
    style:
234
      - collapsed
235
  - name: tag-blue-delete
236
    iconset: fa6-solid
237
    style:
238
      - bookmark
239
      - bookmarked-project
240
    svg: bookmark
241
  - name: tag-blue-add
242
    iconset: fa6-regular
243
    style:
244
      - bookmark-off
245
    svg: bookmark
246
  - name: angle-down
247
    iconset: fa6-solid
248
    style:
249
      - sorted-asc
250
  - name: angle-up
251
    iconset: fa6-solid
252
    style:
253
      - sorted-desc
254
  - name: toggle-plus
255
    iconset: fa6-regular
256
    svg: square-plus
257
  - name: toggle-minus
258
    iconset: fa6-regular
259
    svg: square-minus
260
  - name: clear-query
261
    iconset: fa6-solid
262
    svg: circle-xmark
263
  - name: import
264
    iconset: fa6-solid
265
    svg: file-import
266
  - name: copy-link
267
    iconset: fa6-regular
268
    svg: clipboard
269
  - name: file
270
    iconset: fa6-regular
271
    svg: file
272
background:
273
  - name: bg-angle-up
274
    iconset: fa6-solid
275
    svg: angle-up
276
    color: "#999"
277
  - name: bg-angle-down
278
    iconset: fa6-solid
279
    svg: angle-down
280
    color: "#999"
281
  - name: bg-angle-left
282
    iconset: fa6-solid
283
    svg: angle-left
284
    color: "#999"
285
  - name: bg-angle-right
286
    iconset: fa6-solid
287
    svg: angle-right
288
    color: "#999"
289
  - name: bg-magnifier
290
    iconset: fa6-solid
291
    svg: magnifying-glass
292
    color: "#999"
293
other:
294
  - name: external
295
    iconset: fa6-solid
296
    svg: arrow-up-right-from-square
297
  - name: hourglass
298
    iconset: fa6-solid
299
    svg: hourglass
300
  - name: bullet-go
301
    iconset: mdi
302
    svg: arrow-right-bold
303
  - name: bullet-end
304
    iconset: mdi
305
    svg: arrow-left-bold
306
  - name: bullet-go-end
307
    iconset: mdi
308
    svg: arrow-left-right-bold
309
  - name: atom
310
    iconset: fa6-solid
311
    svg: square-rss
312
  - name: scm-change
313
    iconset: mdi
314
    svg: pencil-circle
315
  - name: scm-move
316
    iconset: fa6-solid
317
    svg: circle-arrow-right
318
plugin:
319
  - name: plugin-tmp
320
    iconset: fa6-solid
321
    svg: cube
322
file:
323
  - name: text-plain
324
    iconset: mdi
325
    svg: file-document-outline
326
  - name: text-x-c
327
    iconset: mdi
328
    svg: language-c
329
  - name: text-x-csharp
330
    iconset: mdi
331
    svg: language-csharp
332
  - name: text-x-java
333
    iconset: mdi
334
    svg: language-java
335
  - name: text-x-javascript
336
    iconset: mdi
337
    svg: language-javascript
338
  - name: text-x-php
339
    iconset: mdi
340
    svg: language-php
341
  - name: text-x-ruby
342
    iconset: mdi
343
    svg: language-ruby
344
  - name: text-xml
345
    iconset: mdi
346
    svg: xml
347
  - name: text-css
348
    iconset: mdi
349
    svg: language-css3
350
  - name: text-html
351
    iconset: mdi
352
    svg: xml
353
  - name: image-gif
354
    iconset: mdi
355
    svg: file-gif-box
356
  - name: image-jpeg
357
    iconset: mdi
358
    svg: file-jpg-box
359
  - name: image-png
360
    iconset: mdi
361
    svg: file-png-box
362
  - name: image-tiff
363
    iconset: mdi
364
    svg: file-image-outline
365
  - name: application-pdf
366
    iconset: mdi
367
    svg: file-pdf-box
368
  - name: application-zip
369
    iconset: mdi
370
    svg: zip-box-outline
371
  - name: application-x-gzip
372
    iconset: mdi
373
    svg: zip-box-outline
374
jstoolbar:
375
  - name: strong
376
    iconset: fa6-solid
377
    svg: bold
378
  - name: em
379
    iconset: fa6-solid
380
    svg: italic
381
  - name: ins
382
    iconset: fa6-solid
383
    svg: underline
384
  - name: del
385
    iconset: fa6-solid
386
    svg: strikethrough
387
  - name: ul
388
    iconset: fa6-solid
389
    svg: list-ul
390
  - name: ol
391
    iconset: fa6-solid
392
    svg: list-ol
393
  - name: tl
394
    iconset: fa6-solid
395
    svg: list-check
396
  - name: bq
397
    iconset: fa6-solid
398
    svg: indent
399
  - name: unbq
400
    iconset: fa6-solid
401
    svg: outdent
402
  - name: table
403
    iconset: fa6-solid
404
    svg: table
405
  - name: precode
406
    iconset: fa6-solid
407
    svg: code
408
  - name: link
409
    iconset: fa6-solid
410
    svg: link
411
  - name: img
412
    iconset: fa6-solid
413
    svg: image
414
  - name: help
415
    iconset: fa6-regular
416
    svg: circle-question
417
# C, H1, H2, H3, PRE is string. not svg icon
lib/redmine/icon.rb
1
# frozen_string_literal: true
2

  
3
# Redmine - project management software
4
# Copyright (C) 2006-  Jean-Philippe Lang
5
#
6
# This program is free software; you can redistribute it and/or
7
# modify it under the terms of the GNU General Public License
8
# as published by the Free Software Foundation; either version 2
9
# of the License, or (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
# GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19

  
20
require 'uri'
21
require 'net/http'
22
require 'fileutils'
23

  
24
module Redmine
25
  class Icon
26
    def initialize(url: , source: , config: , images_dir:, styles_dir:)
27
      @uri = URI.parse url
28
      @source = YAML.safe_load File.open(source)
29
      hash = YAML.safe_load(File.open(config))
30
      @categories = hash.map{|key, val| [key, IconCategory.new(key, val)] }.to_h
31
      @images_dir = images_dir
32
      @styles_dir = styles_dir
33
      @prepared = false
34
    end
35

  
36
    def prepare
37
      return if @prepared
38

  
39
      @iconsets = {}
40
      @icons = []
41
      @source.each do |category, array|
42

  
43
        array.each do |data|
44
          iconset_name = data['iconset']
45
          @iconsets[ iconset_name ] ||= IconSet.new(iconset_name)
46
          icon = SVGIcon.new(data, @iconsets[ iconset_name ], @categories[category])
47

  
48
          @icons << icon
49
          @iconsets[ iconset_name ].icons << icon
50
        end
51
      end
52
      fetch
53
      @prepared = true
54
    end
55

  
56
    def dump_icons
57
      @icons.each {|i| i.dump(@images_dir) }
58
    end
59

  
60
    def dump_css
61
      @categories.values.each {|c| c.to_css(@styles_dir)}
62
    end
63

  
64
    private
65

  
66
    def fetch
67
      http = Net::HTTP.new(@uri.host, @uri.port)
68
      http.use_ssl = true
69
      http.verify_mode = OpenSSL::SSL::VERIFY_NONE
70

  
71
      http.start do |client|
72

  
73
        source = "/collections?prefixes=#{@iconsets.keys.join(',')}"
74
        metadata = fetch_from(client, source)
75

  
76
        @iconsets.each do |prefix, iconset|
77
          iconset.metadata ||= metadata[prefix]
78

  
79
          source = "/#{prefix}.json?icons=#{iconset.icons.map{|i| i.svg_name}.join(',')}"
80
          iconset.data = fetch_from(client, source)
81
        end
82
      end
83
    end
84

  
85
    def fetch_from(client, source)
86
      res = client.get(source)
87

  
88
      case res
89
      when Net::HTTPSuccess
90
        JSON.parse res.body
91
      else
92
        raise StandardError.new('collection api failed!!')
93
      end
94
    end
95

  
96
    class IconCategory
97
      attr_reader :name, :prefix, :dir, :icons, :template
98

  
99
      def initialize(name, obj)
100
        @name   = name
101
        @prefix = obj['prefix']
102
        @dir    = obj['dir']
103
        @template = obj['template']
104
        @icons  = []
105
      end
106

  
107
      def to_css(dir)
108
        FileUtils.mkdir_p dir
109
        css = icons.map{|i| i.css }.select{|c| c != nil}.join("\n")
110

  
111
        File.write dir.join("#{name}.css"), css
112
      end
113
    end
114

  
115
    class IconSet
116
      SVG =<<~EOL
117
      <!-- iconset: %<iconset>s icon: %<svg>s.svg%<version>s license: %<license>s url: %<url>s -->
118
      <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 %<width>d %<height>d">
119
        %<content>s
120
      </svg>
121
      EOL
122

  
123
      attr_accessor :version, :data, :metadata
124
      attr_reader :name, :icons
125

  
126
      def initialize(name)
127
        @name = name
128
        @icons = []
129
      end
130

  
131
      def xml(svg_name, color)
132
        content = data['icons'][svg_name]
133
        raise StandardError.new("Icon not found #{svg_name} in #{name}") unless content
134

  
135
        width   = content['width']  || data['width']
136
        height  = content['height'] || data['height']
137
        body    = content['body']
138
        body    = body.sub('currentColor', color) if color
139
        version = metadata['version'] ? " version: #{metadata['version']}" : nil
140
        format(SVG, iconset: metadata['name'],
141
               version: version,
142
               license: metadata['license']['title'],
143
               svg: svg_name,
144
               url: metadata['author']['url'],
145
               width: width,
146
               height: height,
147
               content: body)
148
      end
149
    end
150

  
151
    class SVGIcon
152
      attr_reader :name, :svg_name, :iconset, :category, :color
153

  
154
      def initialize(obj, iconset, category)
155
        @category = category
156
        @category.icons << self if category
157

  
158
        @name = obj['name']
159
        @svg_name = obj['svg'] || obj['name']
160
        @iconset  = iconset
161
        @selector = obj['style'] || [ obj['name'] ]
162
        @color = obj['color']
163
      end
164

  
165
      def selector_line
166
        @selector.map do |k|
167
          if (k =~ /\./)
168
            k
169
          else
170
            prefix = category.prefix || ''
171
            ".#{prefix}#{k}::before"
172
          end
173
        end
174
      end
175

  
176
      def css
177
        if category
178
          url = "url(/#{category.dir}/#{name}.svg)"
179
          if category.prefix
180
            "#{selector_line.join(', ')} { --icon-image: #{url}}"
181
          elsif category.template
182
            format(category.template, name, url)
183
          end
184
        else
185
          nil
186
        end
187
      end
188

  
189
      def dump(dest)
190
        return nil if category == nil
191

  
192
        dir = dest.join(category.dir)
193

  
194
        FileUtils.mkdir_p dir
195
        File.write dir.join("#{name}.svg"), to_xml
196
      end
197

  
198
      private
199

  
200
      def path
201
        category.dir + '/' + name + '.svg'
202
      end
203

  
204
      def to_xml
205
        iconset.xml svg_name, color
206
      end
207
    end
208
  end
209
end
210

  
lib/tasks/icon.rake
1
# Redmine - project management software
2
# Copyright (C) 2006-  Jean-Philippe Lang
3
#
4
# This program is free software; you can redistribute it and/or
5
# modify it under the terms of the GNU General Public License
6
# as published by the Free Software Foundation; either version 2
7
# of the License, or (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17

  
18
require 'redmine/icon'
19

  
20
namespace :icon do
21

  
22
  @icon = nil
23

  
24
  task :setup do
25
    @icon ||= Redmine::Icon.new url: 'https://api.iconify.design/',
26
      source: Rails.root.join('config', 'icon_source.yml'),
27
      config: Rails.root.join('config', 'icon_config.yml'),
28
      images_dir: Rails.root.join('app/assets/images'),
29
      styles_dir: Rails.root.join('app/assets/stylesheets/icons')
30
    @icon.prepare
31
  end
32

  
33
  desc 'Fetch svg icons from iconify.design'
34
  task :fetch => :setup do
35
    @icon.dump_icons
36
  end
37

  
38
  desc 'Output stylesheets to show svg icon'
39
  task :css => :setup do
40
    @icon.dump_css
41
  end
42
end
(37-37/54)