dragonfly_fonts 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +23 -0
  3. data/Dockerfile +15 -0
  4. data/Gemfile +4 -0
  5. data/Guardfile +8 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +196 -0
  8. data/Rakefile +9 -0
  9. data/circle.yml +23 -0
  10. data/docker-compose.yml +15 -0
  11. data/dragonfly_fonts.gemspec +28 -0
  12. data/lib/dragonfly_fonts.rb +8 -0
  13. data/lib/dragonfly_fonts/analysers/bbox.rb +35 -0
  14. data/lib/dragonfly_fonts/analysers/font_info.rb +15 -0
  15. data/lib/dragonfly_fonts/analysers/glyphs.rb +14 -0
  16. data/lib/dragonfly_fonts/analysers/gsub_tables.rb +14 -0
  17. data/lib/dragonfly_fonts/plugin.rb +43 -0
  18. data/lib/dragonfly_fonts/processors/correct_metrics.rb +21 -0
  19. data/lib/dragonfly_fonts/processors/encode.rb +57 -0
  20. data/lib/dragonfly_fonts/processors/extract_glyph.rb +19 -0
  21. data/lib/dragonfly_fonts/processors/normalize_names.rb +17 -0
  22. data/lib/dragonfly_fonts/processors/set_dimensions.rb +24 -0
  23. data/lib/dragonfly_fonts/processors/set_ttf_names.rb +52 -0
  24. data/lib/dragonfly_fonts/processors/set_underline.rb +26 -0
  25. data/lib/dragonfly_fonts/processors/set_width.rb +25 -0
  26. data/lib/dragonfly_fonts/processors/set_woff_metadata.rb +23 -0
  27. data/lib/dragonfly_fonts/processors/ttf_autohint.rb +21 -0
  28. data/lib/dragonfly_fonts/processors/web_friendly.rb +17 -0
  29. data/lib/dragonfly_fonts/unicode_ranges.rb +89 -0
  30. data/lib/dragonfly_fonts/version.rb +3 -0
  31. data/samples/Arial.ttf +0 -0
  32. data/samples/Inconsolata.otf +0 -0
  33. data/script/dimensions.py +28 -0
  34. data/script/font_info.py +49 -0
  35. data/script/glyphs.py +25 -0
  36. data/script/gsub_tables.py +43 -0
  37. data/script/normalize_names.sh +13 -0
  38. data/script/underline.py +20 -0
  39. data/script/webfonts.pe +49 -0
  40. data/script/woff_meta.py +81 -0
  41. data/test/dragonfly_fonts/analysers/bbox_test.rb +48 -0
  42. data/test/dragonfly_fonts/analysers/font_info_test.rb +65 -0
  43. data/test/dragonfly_fonts/analysers/glyphs_test.rb +27 -0
  44. data/test/dragonfly_fonts/analysers/gsub_tables_test.rb +42 -0
  45. data/test/dragonfly_fonts/plugin_test.rb +76 -0
  46. data/test/dragonfly_fonts/processors/correct_metrics_test.rb +22 -0
  47. data/test/dragonfly_fonts/processors/encode_test.rb +36 -0
  48. data/test/dragonfly_fonts/processors/extract_glyph_test.rb +22 -0
  49. data/test/dragonfly_fonts/processors/normalize_names_test.rb +18 -0
  50. data/test/dragonfly_fonts/processors/set_dimensions_test.rb +26 -0
  51. data/test/dragonfly_fonts/processors/set_ttf_names_test.rb +23 -0
  52. data/test/dragonfly_fonts/processors/set_underline_test.rb +22 -0
  53. data/test/dragonfly_fonts/processors/set_width_test.rb +48 -0
  54. data/test/dragonfly_fonts/processors/set_woff_metadata_test.rb +28 -0
  55. data/test/dragonfly_fonts/processors/ttf_autohint_test.rb +18 -0
  56. data/test/dragonfly_fonts/processors/web_friendly_test.rb +18 -0
  57. data/test/test_helper.rb +21 -0
  58. metadata +215 -0
@@ -0,0 +1,43 @@
1
+ require 'dragonfly_fonts/analysers/bbox'
2
+ require 'dragonfly_fonts/analysers/font_info'
3
+ require 'dragonfly_fonts/analysers/glyphs'
4
+ require 'dragonfly_fonts/analysers/gsub_tables'
5
+
6
+ require 'dragonfly_fonts/processors/correct_metrics'
7
+ require 'dragonfly_fonts/processors/encode'
8
+ require 'dragonfly_fonts/processors/extract_glyph'
9
+ require 'dragonfly_fonts/processors/normalize_names'
10
+ require 'dragonfly_fonts/processors/set_dimensions'
11
+ require 'dragonfly_fonts/processors/set_ttf_names'
12
+ require 'dragonfly_fonts/processors/set_underline'
13
+ require 'dragonfly_fonts/processors/set_width'
14
+ require 'dragonfly_fonts/processors/set_woff_metadata'
15
+ require 'dragonfly_fonts/processors/ttf_autohint'
16
+ require 'dragonfly_fonts/processors/web_friendly'
17
+
18
+ module DragonflyFonts
19
+ class Plugin
20
+ def call(app, _opts = {})
21
+ app.add_analyser :bbox, Analysers::Bbox.new
22
+ app.add_analyser :font_info, Analysers::FontInfo.new
23
+ app.add_analyser :glyphs, Analysers::Glyphs.new
24
+ app.add_analyser :gsub_tables, Analysers::GsubTables.new
25
+
26
+ # ---------------------------------------------------------------------
27
+
28
+ app.add_processor :correct_metrics, Processors::CorrectMetrics.new
29
+ app.add_processor :encode, Processors::Encode.new
30
+ app.add_processor :extract_glyph, Processors::ExtractGlyph.new
31
+ app.add_processor :normalize_names, Processors::NormalizeNames.new
32
+ app.add_processor :set_dimensions, Processors::SetDimensions.new
33
+ app.add_processor :set_ttf_names, Processors::SetTtfNames.new
34
+ app.add_processor :set_underline, Processors::SetUnderline.new
35
+ app.add_processor :set_width, Processors::SetWidth.new
36
+ app.add_processor :set_woff_metadata, Processors::SetWoffMetadata.new
37
+ app.add_processor :ttf_autohint, Processors::TtfAutohint.new
38
+ app.add_processor :web_friendly, Processors::WebFriendly.new
39
+ end
40
+ end
41
+ end
42
+
43
+ Dragonfly::App.register_plugin(:fonts) { DragonflyFonts::Plugin.new }
@@ -0,0 +1,21 @@
1
+ module DragonflyFonts
2
+ module Processors
3
+ class CorrectMetrics
4
+ def call(font)
5
+ font.shell_update do |old_path, new_path|
6
+ "#{fontforge_command} -lang=ff -c 'Open($1); SetOS2Value(\"HHeadAscent\",$ascent); SetOS2Value(\"HHeadAscentIsOffset\",0); SetOS2Value(\"HHeadDescent\",-$descent); SetOS2Value(\"HHeadDescentIsOffset\",0); SetOS2Value(\"TypoLineGap\",0); Generate($2);' #{old_path} #{new_path}"
7
+ end
8
+ end
9
+
10
+ def update_url(attrs, *_args)
11
+ attrs.style = 'corrmetr'
12
+ end
13
+
14
+ private # =============================================================
15
+
16
+ def fontforge_command
17
+ 'fontforge'
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,57 @@
1
+ require 'shellwords'
2
+
3
+ module DragonflyFonts
4
+ module Processors
5
+ class Encode
6
+ class UnsupportedFormat < RuntimeError; end
7
+
8
+ def call(content, format)
9
+ content.shell_update(ext: format, escape: false) do |old_path, new_path|
10
+ case format.to_sym
11
+ when :eot then ttf2eot(old_path, new_path)
12
+ when :otf, :svg, :ttf, :woff then fontforge(old_path, new_path)
13
+ when :woff2 then woff2(old_path, new_path)
14
+ else fail UnsupportedFormat
15
+ end
16
+ end
17
+
18
+ content.meta['format'] = format.to_s
19
+ content.ext = format
20
+ end
21
+
22
+ def update_url(attrs, format, _args = '')
23
+ attrs.ext = format.to_s
24
+ end
25
+
26
+ private # =============================================================
27
+
28
+ def fontforge(old_path, new_path)
29
+ "#{fontforge_command} -lang=ff -c 'Open($1); Generate($2)' #{old_path} #{new_path}"
30
+ end
31
+
32
+ def fontforge_command
33
+ 'fontforge'
34
+ end
35
+
36
+ # ---------------------------------------------------------------------
37
+
38
+ def ttf2eot(old_path, new_path)
39
+ "#{ttf2eot_command} < #{old_path} > #{new_path}"
40
+ end
41
+
42
+ def ttf2eot_command
43
+ 'ttf2eot'
44
+ end
45
+
46
+ # ---------------------------------------------------------------------
47
+
48
+ def woff2(old_path, new_path)
49
+ "#{woff2_command} #{old_path} && mv #{old_path.gsub(/\.\w{3,5}\z/, '.woff2')} #{new_path}"
50
+ end
51
+
52
+ def woff2_command
53
+ 'woff2_compress'
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,19 @@
1
+ module DragonflyFonts
2
+ module Processors
3
+ class ExtractGlyph
4
+ def call(font, glyph, opts = {})
5
+ format = opts.fetch(:format, :svg)
6
+
7
+ font.shell_update(ext: format) do |old_path, new_path|
8
+ "#{fontforge_command} -lang=ff -c 'Open($1); Select(\"#{glyph}\"); Export(\"#{new_path}\");' #{old_path}"
9
+ end
10
+ end
11
+
12
+ private # =============================================================
13
+
14
+ def fontforge_command
15
+ 'fontforge'
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,17 @@
1
+ module DragonflyFonts
2
+ module Processors
3
+ class NormalizeNames
4
+ def call(font, opts = {})
5
+ font.shell_update(ext: :ttf) do |old_path, new_path|
6
+ "#{fontforge_command} -script #{DragonflyFonts::SCRIPT_DIR.join('normalize_names.sh')} #{old_path} #{new_path}"
7
+ end
8
+ end
9
+
10
+ private # =============================================================
11
+
12
+ def fontforge_command
13
+ 'fontforge'
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,24 @@
1
+ module DragonflyFonts
2
+ module Processors
3
+ class SetDimensions
4
+ def call(content, opts = {})
5
+ ascent = opts.fetch(:ascent, '')
6
+ descent = opts.fetch(:descent, '')
7
+
8
+ content.shell_update do |old_path, new_path|
9
+ "#{dimensions_script} #{old_path} #{new_path} #{Shellwords.escape(ascent)} #{Shellwords.escape(descent)}"
10
+ end
11
+ end
12
+
13
+ def update_url(attrs, *_args)
14
+ attrs.style = 'adjdimensions'
15
+ end
16
+
17
+ private # =============================================================
18
+
19
+ def dimensions_script
20
+ DragonflyFonts::SCRIPT_DIR.join('dimensions.py')
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,52 @@
1
+ require 'shellwords'
2
+
3
+ module DragonflyFonts
4
+ module Processors
5
+ class SetTtfNames
6
+ # http://partners.adobe.com/public/developer/opentype/index_name.html#enc4
7
+ NAME_IDS = {
8
+ compatible_full: 18,
9
+ copyright: 0,
10
+ description: 10,
11
+ designer: 9,
12
+ designer_url: 12,
13
+ fontname: 1,
14
+ fullname: 4,
15
+ license: 13,
16
+ license_url: 14,
17
+ manufacturer: 8,
18
+ postscript_cid: 20,
19
+ postscript_name: 6,
20
+ preferred_family: 16,
21
+ preferred_subfamily: 17,
22
+ sample_text: 19,
23
+ trademark: 7,
24
+ uid: 3,
25
+ vendor_url: 11,
26
+ version: 5,
27
+ weight: 2
28
+ }
29
+
30
+ def call(font, values = {})
31
+ font.shell_update do |old_path, new_path|
32
+ "#{fontforge_command} -lang=ff -c 'Open($1); #{command_string(values)} Generate($2);' #{old_path} #{new_path}"
33
+ end
34
+ end
35
+
36
+ private # =============================================================
37
+
38
+ def fontforge_command
39
+ 'fontforge'
40
+ end
41
+
42
+ def command_string(values)
43
+ res = []
44
+ values.each do |k, v|
45
+ next unless NAME_IDS.keys.include?(k.to_sym)
46
+ res << "SetTTFName(0x409, #{NAME_IDS[k]}, \"#{Shellwords.escape(v)}\");"
47
+ end
48
+ res.join
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,26 @@
1
+ require 'shellwords'
2
+
3
+ module DragonflyFonts
4
+ module Processors
5
+ class SetUnderline
6
+ def call(content, opts = {})
7
+ upos = opts.fetch(:upos, '')
8
+ uwidth = opts.fetch(:uwidth, '')
9
+
10
+ content.shell_update do |old_path, new_path|
11
+ "#{underline_script} #{old_path} #{new_path} #{Shellwords.escape(upos)} #{Shellwords.escape(uwidth)}"
12
+ end
13
+ end
14
+
15
+ def update_url(attrs, *_args)
16
+ attrs.style = 'adjunderline'
17
+ end
18
+
19
+ private # =============================================================
20
+
21
+ def underline_script
22
+ DragonflyFonts::SCRIPT_DIR.join('underline.py')
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,25 @@
1
+ module DragonflyFonts
2
+ module Processors
3
+ class SetWidth
4
+ # relative:
5
+ # when absent or 0 the vertical width will be set to width
6
+ # when 1 then the vertical width will be incremented by the first
7
+ # when 2 then the vertical width will be scaled by <first argument>/100.0.
8
+ def call(content, width, relative = 1)
9
+ content.shell_update do |old_path, new_path|
10
+ "#{fontforge_command} -lang=ff -c 'Open($1); SelectWorthOutputting(); SetWidth(#{width},#{relative}); Generate($2);' #{old_path} #{new_path}"
11
+ end
12
+ end
13
+
14
+ def update_url(attrs, *_args)
15
+ attrs.style = 'adjwidth'
16
+ end
17
+
18
+ private # =============================================================
19
+
20
+ def fontforge_command
21
+ 'fontforge'
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,23 @@
1
+ require 'shellwords'
2
+
3
+ module DragonflyFonts
4
+ module Processors
5
+ class SetWoffMetadata
6
+ def call(font, uniqueid, licensee_name = '')
7
+ font.shell_update(ext: :woff) do |old_path, new_path|
8
+ "#{woff_meta_script} #{old_path} #{new_path} #{Shellwords.escape(uniqueid)} #{Shellwords.escape(licensee_name)}"
9
+ end
10
+ end
11
+
12
+ def update_url(attrs, *_args)
13
+ attrs.style = 'woffmeta'
14
+ end
15
+
16
+ private # =============================================================
17
+
18
+ def woff_meta_script
19
+ DragonflyFonts::SCRIPT_DIR.join('woff_meta.py')
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,21 @@
1
+ module DragonflyFonts
2
+ module Processors
3
+ class TtfAutohint
4
+ def call(font)
5
+ font.shell_update do |old_path, new_path|
6
+ "#{ttfautohint_command} --strong-stem-width='' --windows-compatibility --composites #{old_path} #{new_path}"
7
+ end
8
+ end
9
+
10
+ def update_url(attrs, *_args)
11
+ attrs.style = 'ttfautohint'
12
+ end
13
+
14
+ private # =============================================================
15
+
16
+ def ttfautohint_command
17
+ 'ttfautohint'
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,17 @@
1
+ module DragonflyFonts
2
+ module Processors
3
+ class WebFriendly
4
+ def call(font, opts = {})
5
+ font.shell_update(ext: :ttf) do |old_path, new_path|
6
+ "#{fontforge_command} -script #{DragonflyFonts::SCRIPT_DIR.join('webfonts.pe')} #{old_path} #{new_path}"
7
+ end
8
+ end
9
+
10
+ private # =============================================================
11
+
12
+ def fontforge_command
13
+ 'fontforge'
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,89 @@
1
+ DragonflyFonts::UNICODE_RANGES = [
2
+ ['Basic Latin', ['0x0000-0x007F', '0-127']],
3
+ ['Latin-1 Supplement', ['0x0080-0x00FF', '128-255']],
4
+ ['Latin Extended-A', ['0x0100-0x017F', '256-383']],
5
+ ['Latin Extended-B', ['0x0180-0x024F', '384-591']],
6
+ ['IPA Extensions', ['0x0250-0x02AF', '592-687']],
7
+ ['Spacing Modifier Letters', ['0x02B0-0x02FF', '688-767']],
8
+ ['Combining Diacritical Marks', ['0x0300-0x036F', '768-879']],
9
+ ['Greek', ['0x0370-0x03FF', '880-1023']],
10
+ ['Cyrillic', ['0x0400-0x04FF', '1024-1279']],
11
+ ['Armenian', ['0x0530-0x058F', '1328-1423']],
12
+ ['Hebrew', ['0x0590-0x05FF', '1424-1535']],
13
+ ['Arabic', ['0x0600-0x06FF', '1536-1791']],
14
+ ['Syriac', ['0x0700-0x074F', '1792-1871']],
15
+ ['Thaana', ['0x0780-0x07BF', '1920-1983']],
16
+ ['Devanagari', ['0x0900-0x097F', '2304-2431']],
17
+ ['Bengali', ['0x0980-0x09FF', '2432-2559']],
18
+ ['Gurmukhi', ['0x0A00-0x0A7F', '2560-2687']],
19
+ ['Gujarati', ['0x0A80-0x0AFF', '2688-2815']],
20
+ ['Oriya', ['0x0B00-0x0B7F', '2816-2943']],
21
+ ['Tamil', ['0x0B80-0x0BFF', '2944-3071']],
22
+ ['Telugu', ['0x0C00-0x0C7F', '3072-3199']],
23
+ ['Kannada', ['0x0C80-0x0CFF', '3200-3327']],
24
+ ['Malayalam', ['0x0D00-0x0D7F', '3328-3455']],
25
+ ['Sinhala', ['0x0D80-0x0DFF', '3456-3583']],
26
+ ['Thai', ['0x0E00-0x0E7F', '3584-3711']],
27
+ ['Lao', ['0x0E80-0x0EFF', '3712-3839']],
28
+ ['Tibetan', ['0x0F00-0x0FFF', '3840-4095']],
29
+ ['Myanmar', ['0x1000-0x109F', '4096-4255']],
30
+ ['Georgian', ['0x10A0-0x10FF', '4256-4351']],
31
+ ['Hangul Jamo', ['0x1100-0x11FF', '4352-4607']],
32
+ ['Ethiopic', ['0x1200-0x137F', '4608-4991']],
33
+ ['Cherokee', ['0x13A0-0x13FF', '5024-5119']],
34
+ ['Unified Canadian Aboriginal Syllabics', ['0x1400-0x167F', '5120-5759']],
35
+ ['Ogham', ['0x1680-0x169F', '5760-5791']],
36
+ ['Runic', ['0x16A0-0x16FF', '5792-5887']],
37
+ ['Khmer', ['0x1780-0x17FF', '6016-6143']],
38
+ ['Mongolian', ['0x1800-0x18AF', '6144-6319']],
39
+ ['Latin Extended Additional', ['0x1E00-0x1EFF', '7680-7935']],
40
+ ['Greek Extended', ['0x1F00-0x1FFF', '7936-8191']],
41
+ ['General Punctuation', ['0x2000-0x206F', '8192-8303']],
42
+ ['Superscripts and Subscripts', ['0x2070-0x209F', '8304-8351']],
43
+ ['Currency Symbols', ['0x20A0-0x20CF', '8352-8399']],
44
+ ['Combining Marks for Symbols', ['0x20D0-0x20FF', '8400-8447']],
45
+ ['Letterlike Symbols', ['0x2100-0x214F', '8448-8527']],
46
+ ['Number Forms', ['0x2150-0x218F', '8528-8591']],
47
+ ['Arrows', ['0x2190-0x21FF', '8592-8703']],
48
+ ['Mathematical Operators', ['0x2200-0x22FF', '8704-8959']],
49
+ ['Miscellaneous Technical', ['0x2300-0x23FF', '8960-9215']],
50
+ ['Control Pictures', ['0x2400-0x243F', '9216-9279']],
51
+ ['Optical Character Recognition', ['0x2440-0x245F', '9280-9311']],
52
+ ['Enclosed Alphanumerics', ['0x2460-0x24FF', '9312-9471']],
53
+ ['Box Drawing', ['0x2500-0x257F', '9472-9599']],
54
+ ['Block Elements', ['0x2580-0x259F', '9600-9631']],
55
+ ['Geometric Shapes', ['0x25A0-0x25FF', '9632-9727']],
56
+ ['Miscellaneous Symbols', ['0x2600-0x26FF', '9728-9983']],
57
+ ['Dingbats', ['0x2700-0x27BF', '9984-10175']],
58
+ ['Braille Patterns', ['0x2800-0x28FF', '10240-10495']],
59
+ ['CJK Radicals Supplement', ['0x2E80-0x2EFF', '11904-12031']],
60
+ ['Kangxi Radicals', ['0x2F00-0x2FDF', '12032-12255']],
61
+ ['Ideographic Description Characters', ['0x2FF0-0x2FFF', '12272-12287']],
62
+ ['CJK Symbols and Punctuation', ['0x3000-0x303F', '12288-12351']],
63
+ ['Hiragana', ['0x3040-0x309F', '12352-12447']],
64
+ ['Katakana', ['0x30A0-0x30FF', '12448-12543']],
65
+ ['Bopomofo', ['0x3100-0x312F', '12544-12591']],
66
+ ['Hangul Compatibility Jamo', ['0x3130-0x318F', '12592-12687']],
67
+ ['Kanbun', ['0x3190-0x319F', '12688-12703']],
68
+ ['Bopomofo Extended', ['0x31A0-0x31BF', '12704-12735']],
69
+ ['Enclosed CJK Letters and Months', ['0x3200-0x32FF', '12800-13055']],
70
+ ['CJK Compatibility', ['0x3300-0x33FF', '13056-13311']],
71
+ ['CJK Unified Ideographs Extension A', ['0x3400-0x4DB5', '13312-19893']],
72
+ ['CJK Unified Ideographs', ['0x4E00-0x9FFF', '19968-40959']],
73
+ ['Yi Syllables', ['0xA000-0xA48F', '40960-42127']],
74
+ ['Yi Radicals', ['0xA490-0xA4CF', '42128-42191']],
75
+ ['Hangul Syllables', ['0xAC00-0xD7A3', '44032-55203']],
76
+ ['High Surrogates', ['0xD800-0xDB7F', '55296-56191']],
77
+ ['High Private Use Surrogates', ['0xDB80-0xDBFF', '56192-56319']],
78
+ ['Low Surrogates', ['0xDC00-0xDFFF', '56320-57343']],
79
+ ['Private Use', ['0xE000-0xF8FF', '57344-63743']],
80
+ ['CJK Compatibility Ideographs', ['0xF900-0xFAFF', '63744-64255']],
81
+ ['Alphabetic Presentation Forms', ['0xFB00-0xFB4F', '64256-64335']],
82
+ ['Arabic Presentation Forms-A', ['0xFB50-0xFDFF', '64336-65023']],
83
+ ['Combining Half Marks', ['0xFE20-0xFE2F', '65056-65071']],
84
+ ['CJK Compatibility Forms', ['0xFE30-0xFE4F', '65072-65103']],
85
+ ['Small Form Variants', ['0xFE50-0xFE6F', '65104-65135']],
86
+ ['Arabic Presentation Forms-B', ['0xFE70-0xFEFE', '65136-65278']],
87
+ ['Specials', ['0xFFF0-0xFFFD', '65520-65533']],
88
+ ['Halfwidth and Fullwidth Forms', ['0xFF00-0xFFEF', '65280-65519']]
89
+ ]