├── lib ├── text_search.dart └── src │ ├── jaro_winkler.dart │ └── text_search_base.dart ├── .gitignore ├── pubspec.yaml ├── analysis_options.yaml ├── example └── text_search_example.dart ├── CHANGELOG.md ├── README.md ├── LICENSE ├── test └── text_search_test.dart └── pubspec.lock /lib/text_search.dart: -------------------------------------------------------------------------------- 1 | /// Enables simple partial text search. 2 | library text_search; 3 | 4 | export 'src/text_search_base.dart'; 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Files and directories created by pub 2 | .dart_tool/ 3 | .packages 4 | 5 | # Conventional directory for build outputs 6 | build/ 7 | 8 | # Directory created by dartdoc 9 | doc/api/ 10 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: text_search 2 | description: Simple in-memory fuzzy text searching library. 3 | version: 1.0.2 4 | 5 | repository: https://github.com/FlutterFlow/text_search 6 | 7 | environment: 8 | sdk: '>=2.12.0 <4.0.0' 9 | 10 | dependencies: 11 | tuple: ^2.0.0 12 | 13 | dev_dependencies: 14 | pedantic: 15 | test: 16 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # Defines a default set of lint rules enforced for 2 | # projects at Google. For details and rationale, 3 | # see https://github.com/dart-lang/pedantic#enabled-lints. 4 | include: package:pedantic/analysis_options.yaml 5 | 6 | # For lint rules and documentation, see http://dart-lang.github.io/linter/lints. 7 | # Uncomment to specify additional rules. 8 | # linter: 9 | # rules: 10 | # - camel_case_types 11 | 12 | analyzer: 13 | # exclude: 14 | # - path/to/excluded/files/** 15 | -------------------------------------------------------------------------------- /example/text_search_example.dart: -------------------------------------------------------------------------------- 1 | import 'package:text_search/text_search.dart'; 2 | 3 | final searchableItems = [ 4 | TextSearchItem.fromTerms( 5 | 'Coffee Shop', ['coffee', 'latte', 'macchiato', 'tea']), 6 | TextSearchItem.fromTerms('Dessert', ['ice cream', 'cake', 'pastry']), 7 | TextSearchItem.fromTerms('Milk Tea Shop', ['boba', 'milk tea', 'bubble tea']), 8 | ]; 9 | final placeTypeSearch = TextSearch(searchableItems); 10 | void main() { 11 | print(placeTypeSearch.search('icecream')); 12 | } 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.0.2 2 | 3 | - Add an option to always match prefix. 4 | 5 | ## 1.0.1 6 | 7 | - Fixes additional edge cases. 8 | 9 | ## 1.0.0 10 | 11 | - Update major version since 0.0.3 had breaking changes. 12 | - Fix some edge cases. 13 | 14 | ## 0.0.3 15 | 16 | - Remove dependency on unmaintained `edit_distance` library. 17 | - Improve text matching results involving spaces in the search term. 18 | 19 | ## 0.0.2 20 | 21 | - Null safety. 22 | ## 0.0.1 23 | 24 | - Initial version. Simple fuzzy text search using `JaroWrinkler` edit distance. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | An in-memory fuzzy text search library for Dart. 2 | 3 | [license](https://github.com/dart-lang/stagehand/blob/master/LICENSE). 4 | 5 | ## Usage 6 | 7 | A simple usage example: 8 | 9 | ```dart 10 | import 'package:text_search/text_search.dart'; 11 | 12 | main() { 13 | final searchableItems = [ 14 | TextSearchItem.fromTerms('Coffee Shop', ['coffee', 'latte', 'macchiato', 'tea']), 15 | TextSearchItem.fromTerms('Dessert', ['ice cream', 'cake', 'pastry']), 16 | TextSearchItem.fromTerms('Milk Tea Shop', ['boba', 'milk tea', 'bubble tea']), 17 | ]; 18 | 19 | final placeTypeSearch = TextSearch(searchableItems); 20 | print(placeTypeSearch.search('icecream')); 21 | } 22 | ``` 23 | 24 | ## Features and bugs 25 | 26 | Please file feature requests and bugs at the [issue tracker][tracker]. 27 | 28 | [tracker]: https://github.com/FlutterFlow/text_search/issues 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2020, Taste App 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /lib/src/jaro_winkler.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | class JaroWinkler { 4 | final double _scalingFactor; 5 | 6 | JaroWinkler([this._scalingFactor = 0.1]); 7 | 8 | double similarity(String s1, String s2) { 9 | if (s1.isEmpty || s2.isEmpty) { 10 | return 0.0; 11 | } 12 | if (s1 == s2) { 13 | return 1.0; 14 | } 15 | 16 | var matchDistance = (s1.length / 2).ceil() - 1; 17 | var s1Matches = List.filled(s1.length, false); 18 | var s2Matches = List.filled(s2.length, false); 19 | 20 | var matches = 0; 21 | var transpositions = 0; 22 | 23 | for (var i = 0; i < s1.length; i++) { 24 | int start = max(0, i - matchDistance); 25 | int end = min(s2.length - 1, i + matchDistance); 26 | 27 | for (var j = start; j <= end; j++) { 28 | if (s2Matches[j]) continue; 29 | 30 | if (s1[i] != s2[j]) continue; 31 | 32 | s1Matches[i] = true; 33 | s2Matches[j] = true; 34 | 35 | matches++; 36 | break; 37 | } 38 | } 39 | 40 | if (matches == 0) return 0.0; 41 | 42 | var k = 0; 43 | for (var i = 0; i < s1.length; i++) { 44 | if (!s1Matches[i]) continue; 45 | 46 | while (!s2Matches[k]) { 47 | k++; 48 | } 49 | 50 | if (s1[i] != s2[k]) transpositions++; 51 | 52 | k++; 53 | } 54 | 55 | var jaro = ((matches / s1.length) + 56 | (matches / s2.length) + 57 | ((matches - transpositions / 2) / matches)) / 58 | 3.0; 59 | 60 | var prefix = 0; 61 | for (var i = 0; i < min(min(4, s1.length), s2.length); i++) { 62 | if (s1[i] == s2[i]) { 63 | prefix++; 64 | } else { 65 | break; 66 | } 67 | } 68 | 69 | return jaro + (prefix * _scalingFactor * (1 - jaro)); 70 | } 71 | 72 | double distance(String s1, String s2) { 73 | return 1 - similarity(s1, s2); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /test/text_search_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | import 'package:text_search/text_search.dart'; 3 | 4 | void main() { 5 | group('TextSearch', () { 6 | // Sample search items 7 | var coffeeShop = TextSearchItem.fromTerms( 8 | 'Coffee Shop', ['coffee', 'latte', 'espresso']); 9 | var teaHouse = 10 | TextSearchItem.fromTerms('Tea House', ['tea', 'chai', 'matcha']); 11 | var teaShop = 12 | TextSearchItem.fromTerms('Tea Shop', ['tea', 'chai', 'matcha']); 13 | var library = 14 | TextSearchItem.fromTerms('Library', ['library', 'books', 'reading']); 15 | var items = [coffeeShop, teaHouse, teaShop, library]; 16 | var search = TextSearch(items); 17 | 18 | test('exact match returns a score of 0', () { 19 | var results = search.search('coffee'); 20 | expect(results.length, greaterThan(0)); 21 | expect(results[0].score, 0.0); 22 | expect(results[0].object, 'Coffee Shop'); 23 | }); 24 | 25 | test('fuzzy match', () { 26 | var results = search.search('cofee'); 27 | expect(results.length, greaterThan(0)); 28 | expect(results[0].object, 'Coffee Shop'); 29 | }); 30 | 31 | test('fastSearch returns items without scores', () { 32 | var results = search.fastSearch('coffe'); 33 | expect(results, contains('Coffee Shop')); 34 | expect(results.length, 1); 35 | }); 36 | 37 | test('search with no matches due to threshold', () { 38 | var results = search.search('coff', matchThreshold: 0.2); 39 | expect(results, isEmpty); 40 | }); 41 | 42 | test('fastSearch with no matches due to threshold', () { 43 | var results = search.fastSearch('coff', matchThreshold: 0.2); 44 | expect(results, isEmpty); 45 | }); 46 | 47 | test('non-matching term returns empty list', () { 48 | var results = search.search('zxcvbnm'); 49 | expect(results, isEmpty); 50 | }); 51 | 52 | test('items returned are sorted by increasing score', () { 53 | var results = search.search('tea'); 54 | expect(results.length, greaterThan(1)); 55 | for (var i = 0; i < results.length - 1; i++) { 56 | expect(results[i].score, lessThanOrEqualTo(results[i + 1].score)); 57 | } 58 | }); 59 | 60 | test('regression test for fuzzy match', () { 61 | final search = TextSearch([ 62 | TextSearchItem.fromTerms('FAB', ['FloatingActionButton']), 63 | ]); 64 | expect(search.search('flo').length, 1); 65 | expect(search.search('floating action').length, 1); 66 | expect(search.search('flating').length, 1); 67 | }); 68 | 69 | test('test matching with spaces', () { 70 | final search = TextSearch([ 71 | TextSearchItem.fromTerms('FAB', ['floating action button']), 72 | ]); 73 | expect(search.search('floatingaction').length, 1); 74 | expect(search.search('floating action').length, 1); 75 | expect(search.search('flating').length, 1); 76 | }); 77 | 78 | test('test edge cases', () { 79 | final search = TextSearch([ 80 | TextSearchItem.fromTerms('single letter term', ['f']), 81 | ]); 82 | expect(search.search('f').length, 1); 83 | expect(search.search('floating').length, 1); 84 | expect(search.search('test').length, 0); 85 | }); 86 | 87 | test('test prefix matching', () { 88 | final search = TextSearch([ 89 | TextSearchItem.fromTerms('foo', ['selectedDaysDifference']), 90 | ]); 91 | // No match because of threshold 92 | expect(search.search('selected', matchThreshold: 0.5).length, 0); 93 | expect(search.search('selectedDays', matchThreshold: 0.5).length, 0); 94 | // Match because alwaysMatchPrefix is true 95 | expect( 96 | search 97 | .search('s', matchThreshold: 0.5, alwaysMatchPrefix: true) 98 | .length, 99 | 1); 100 | expect( 101 | search 102 | .search('selecte', matchThreshold: 0.5, alwaysMatchPrefix: true) 103 | .length, 104 | 1); 105 | expect( 106 | search 107 | .search('selectedDays', 108 | matchThreshold: 0.5, alwaysMatchPrefix: true) 109 | .length, 110 | 1); 111 | expect( 112 | search 113 | .search('selectedDaysDifference', 114 | matchThreshold: 0.5, alwaysMatchPrefix: true) 115 | .length, 116 | 1); 117 | expect( 118 | search 119 | .search('selectedDaysDifferenceAndMore', 120 | matchThreshold: 0.5, alwaysMatchPrefix: true) 121 | .length, 122 | 0); 123 | }); 124 | }); 125 | } 126 | -------------------------------------------------------------------------------- /lib/src/text_search_base.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math' as math; 2 | 3 | import 'package:text_search/src/jaro_winkler.dart'; 4 | import 'package:tuple/tuple.dart'; 5 | 6 | /// Represents a candidate `term` for a search result. 7 | class TextSearchItemTerm { 8 | const TextSearchItemTerm(this.term, [this.scorePenalty = 0.0]); 9 | 10 | final String term; 11 | final double scorePenalty; 12 | 13 | @override 14 | String toString() { 15 | return 'TextSearchItemTerm($term, $scorePenalty)'; 16 | } 17 | } 18 | 19 | /// Represents a single item that can be searched for. The `terms` are all 20 | /// variants that match the item. For e.g. an item `PlaceType.coffee_shop` 21 | /// could have terms: 'coffee', 'latte', etc. 22 | class TextSearchItem { 23 | final T object; 24 | final Iterable terms; 25 | 26 | const TextSearchItem(this.object, this.terms); 27 | factory TextSearchItem.fromTerms(T object, Iterable terms) => 28 | TextSearchItem(object, terms.map((x) => TextSearchItemTerm(x))); 29 | } 30 | 31 | /// A search result containing the matching `object` along with the `score`. 32 | class TextSearchResult { 33 | final T object; 34 | final double score; 35 | 36 | TextSearchResult(this.object, this.score); 37 | 38 | @override 39 | String toString() => 'TextSearchResult($object, $score)'; 40 | } 41 | 42 | /// Used for doing simple in-memory text searching based on a given set of 43 | /// `TextSearchItem`s. Lower scores are better, with exact case-insensitive 44 | /// matches scoring 0. Uses `JaroWinkler` distance. 45 | class TextSearch { 46 | TextSearch(this.items); 47 | static final _editDistance = JaroWinkler(); 48 | 49 | final List> items; 50 | 51 | /// Returns search results along with score ordered by decreasing score. 52 | /// For libraries with 10k+ items, `fastSearch` will start being noticeably 53 | /// faster. 54 | List> search(String term, 55 | {double matchThreshold = 1.5, alwaysMatchPrefix = false}) { 56 | return items 57 | .map((item) => Tuple2( 58 | item, 59 | item.terms 60 | .map((itemTerm) => _scoreTerm(term, itemTerm, 61 | alwaysMatchPrefix: alwaysMatchPrefix)) 62 | .reduce(math.min))) 63 | .where((t) => t.item2 < matchThreshold) 64 | .map((t) => TextSearchResult(t.item1.object, t.item2)) 65 | .toList() 66 | ..sort((a, b) => a.score.compareTo(b.score)); 67 | } 68 | 69 | /// Returns search results ordered by decreasing score. 70 | /// ~3-5x faster than `search`, but does not include the search score. 71 | List fastSearch(String term, 72 | {double matchThreshold = 1.5, alwaysMatchPrefix = false}) { 73 | final sorted = items 74 | .map((item) => Tuple2( 75 | item, 76 | item.terms 77 | .map((itemTerm) => _scoreTerm(term, itemTerm, 78 | alwaysMatchPrefix: alwaysMatchPrefix)) 79 | .reduce(math.min))) 80 | .toList() 81 | ..sort((a, b) => a.item2.compareTo(b.item2)); 82 | final result = []; 83 | for (final candidate in sorted) { 84 | if (candidate.item2 >= matchThreshold) { 85 | break; 86 | } 87 | result.add(candidate.item1.object); 88 | } 89 | return result; 90 | } 91 | 92 | double _scoreTerm(String searchTerm, TextSearchItemTerm itemTerm, 93 | {alwaysMatchPrefix = false}) { 94 | if (itemTerm.term.length == 1) { 95 | return searchTerm.startsWith(itemTerm.term) 96 | ? 0 + itemTerm.scorePenalty 97 | : 4; 98 | } 99 | if (alwaysMatchPrefix && itemTerm.term.startsWith(searchTerm)) { 100 | return 0 + itemTerm.scorePenalty; 101 | } 102 | searchTerm = searchTerm.toLowerCase(); 103 | final term = itemTerm.term.toLowerCase(); 104 | if (searchTerm == term) { 105 | return 0 + itemTerm.scorePenalty; 106 | } 107 | // Direct comparison (regardless of word or sentence). 108 | final initialScore = 109 | _editDistance.distance(searchTerm.toLowerCase(), term.toLowerCase()) * 110 | searchTerm.length; 111 | if (!term.contains(' ')) { 112 | return initialScore + itemTerm.scorePenalty; 113 | } 114 | if (term.startsWith(searchTerm)) { 115 | return math.max(0.05, (0.5 - searchTerm.length / term.length)) + 116 | itemTerm.scorePenalty; 117 | } 118 | if (term.contains(searchTerm)) { 119 | return math.max(0.05, (0.7 - searchTerm.length / term.length)) + 120 | itemTerm.scorePenalty; 121 | } 122 | // Compare to sentences by splitting to each component word. 123 | final words = term.split(' '); 124 | final consideredWords = words.where((word) => word.length > 1); 125 | if (consideredWords.isEmpty) { 126 | return itemTerm.scorePenalty; 127 | } 128 | final perWordScore = consideredWords 129 | .map( 130 | (word) => 131 | // Penalize longer sentences and avoid multiply by 0 (exact match). 132 | math.sqrt(words.length + 1) * 133 | (0.1 + 134 | _scoreTerm(searchTerm, 135 | TextSearchItemTerm(word, itemTerm.scorePenalty))), 136 | ) 137 | .reduce(math.min); 138 | final scoreWithoutEmptySpaces = _scoreTerm( 139 | searchTerm.replaceAll(' ', ''), 140 | TextSearchItemTerm(term.replaceAll(' ', ''), itemTerm.scorePenalty), 141 | ); 142 | return math.min( 143 | scoreWithoutEmptySpaces, math.min(initialScore, perWordScore)) + 144 | itemTerm.scorePenalty; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | _fe_analyzer_shared: 5 | dependency: transitive 6 | description: 7 | name: _fe_analyzer_shared 8 | sha256: "36a321c3d2cbe01cbcb3540a87b8843846e0206df3e691fa7b23e19e78de6d49" 9 | url: "https://pub.dev" 10 | source: hosted 11 | version: "65.0.0" 12 | analyzer: 13 | dependency: transitive 14 | description: 15 | name: analyzer 16 | sha256: dfe03b90ec022450e22513b5e5ca1f01c0c01de9c3fba2f7fd233cb57a6b9a07 17 | url: "https://pub.dev" 18 | source: hosted 19 | version: "6.3.0" 20 | args: 21 | dependency: transitive 22 | description: 23 | name: args 24 | sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 25 | url: "https://pub.dev" 26 | source: hosted 27 | version: "2.4.2" 28 | async: 29 | dependency: transitive 30 | description: 31 | name: async 32 | sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" 33 | url: "https://pub.dev" 34 | source: hosted 35 | version: "2.11.0" 36 | boolean_selector: 37 | dependency: transitive 38 | description: 39 | name: boolean_selector 40 | sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" 41 | url: "https://pub.dev" 42 | source: hosted 43 | version: "2.1.1" 44 | collection: 45 | dependency: transitive 46 | description: 47 | name: collection 48 | sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a 49 | url: "https://pub.dev" 50 | source: hosted 51 | version: "1.18.0" 52 | convert: 53 | dependency: transitive 54 | description: 55 | name: convert 56 | sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" 57 | url: "https://pub.dev" 58 | source: hosted 59 | version: "3.1.1" 60 | coverage: 61 | dependency: transitive 62 | description: 63 | name: coverage 64 | sha256: "0cec992efb6feaa6141779723bf0119d5f511ebd658aa5225d9bac53cb7e609e" 65 | url: "https://pub.dev" 66 | source: hosted 67 | version: "1.7.0" 68 | crypto: 69 | dependency: transitive 70 | description: 71 | name: crypto 72 | sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab 73 | url: "https://pub.dev" 74 | source: hosted 75 | version: "3.0.3" 76 | file: 77 | dependency: transitive 78 | description: 79 | name: file 80 | sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" 81 | url: "https://pub.dev" 82 | source: hosted 83 | version: "7.0.0" 84 | frontend_server_client: 85 | dependency: transitive 86 | description: 87 | name: frontend_server_client 88 | sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" 89 | url: "https://pub.dev" 90 | source: hosted 91 | version: "3.2.0" 92 | glob: 93 | dependency: transitive 94 | description: 95 | name: glob 96 | sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" 97 | url: "https://pub.dev" 98 | source: hosted 99 | version: "2.1.2" 100 | http_multi_server: 101 | dependency: transitive 102 | description: 103 | name: http_multi_server 104 | sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" 105 | url: "https://pub.dev" 106 | source: hosted 107 | version: "3.2.1" 108 | http_parser: 109 | dependency: transitive 110 | description: 111 | name: http_parser 112 | sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" 113 | url: "https://pub.dev" 114 | source: hosted 115 | version: "4.0.2" 116 | io: 117 | dependency: transitive 118 | description: 119 | name: io 120 | sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" 121 | url: "https://pub.dev" 122 | source: hosted 123 | version: "1.0.4" 124 | js: 125 | dependency: transitive 126 | description: 127 | name: js 128 | sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 129 | url: "https://pub.dev" 130 | source: hosted 131 | version: "0.6.7" 132 | logging: 133 | dependency: transitive 134 | description: 135 | name: logging 136 | sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" 137 | url: "https://pub.dev" 138 | source: hosted 139 | version: "1.2.0" 140 | matcher: 141 | dependency: transitive 142 | description: 143 | name: matcher 144 | sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" 145 | url: "https://pub.dev" 146 | source: hosted 147 | version: "0.12.16" 148 | meta: 149 | dependency: transitive 150 | description: 151 | name: meta 152 | sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 153 | url: "https://pub.dev" 154 | source: hosted 155 | version: "1.11.0" 156 | mime: 157 | dependency: transitive 158 | description: 159 | name: mime 160 | sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e 161 | url: "https://pub.dev" 162 | source: hosted 163 | version: "1.0.4" 164 | node_preamble: 165 | dependency: transitive 166 | description: 167 | name: node_preamble 168 | sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" 169 | url: "https://pub.dev" 170 | source: hosted 171 | version: "2.0.2" 172 | package_config: 173 | dependency: transitive 174 | description: 175 | name: package_config 176 | sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" 177 | url: "https://pub.dev" 178 | source: hosted 179 | version: "2.1.0" 180 | path: 181 | dependency: transitive 182 | description: 183 | name: path 184 | sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" 185 | url: "https://pub.dev" 186 | source: hosted 187 | version: "1.8.3" 188 | pedantic: 189 | dependency: "direct dev" 190 | description: 191 | name: pedantic 192 | sha256: "67fc27ed9639506c856c840ccce7594d0bdcd91bc8d53d6e52359449a1d50602" 193 | url: "https://pub.dev" 194 | source: hosted 195 | version: "1.11.1" 196 | pool: 197 | dependency: transitive 198 | description: 199 | name: pool 200 | sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" 201 | url: "https://pub.dev" 202 | source: hosted 203 | version: "1.5.1" 204 | pub_semver: 205 | dependency: transitive 206 | description: 207 | name: pub_semver 208 | sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" 209 | url: "https://pub.dev" 210 | source: hosted 211 | version: "2.1.4" 212 | shelf: 213 | dependency: transitive 214 | description: 215 | name: shelf 216 | sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 217 | url: "https://pub.dev" 218 | source: hosted 219 | version: "1.4.1" 220 | shelf_packages_handler: 221 | dependency: transitive 222 | description: 223 | name: shelf_packages_handler 224 | sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" 225 | url: "https://pub.dev" 226 | source: hosted 227 | version: "3.0.2" 228 | shelf_static: 229 | dependency: transitive 230 | description: 231 | name: shelf_static 232 | sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e 233 | url: "https://pub.dev" 234 | source: hosted 235 | version: "1.1.2" 236 | shelf_web_socket: 237 | dependency: transitive 238 | description: 239 | name: shelf_web_socket 240 | sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" 241 | url: "https://pub.dev" 242 | source: hosted 243 | version: "1.0.4" 244 | source_map_stack_trace: 245 | dependency: transitive 246 | description: 247 | name: source_map_stack_trace 248 | sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae" 249 | url: "https://pub.dev" 250 | source: hosted 251 | version: "2.1.1" 252 | source_maps: 253 | dependency: transitive 254 | description: 255 | name: source_maps 256 | sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" 257 | url: "https://pub.dev" 258 | source: hosted 259 | version: "0.10.12" 260 | source_span: 261 | dependency: transitive 262 | description: 263 | name: source_span 264 | sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" 265 | url: "https://pub.dev" 266 | source: hosted 267 | version: "1.10.0" 268 | stack_trace: 269 | dependency: transitive 270 | description: 271 | name: stack_trace 272 | sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" 273 | url: "https://pub.dev" 274 | source: hosted 275 | version: "1.11.1" 276 | stream_channel: 277 | dependency: transitive 278 | description: 279 | name: stream_channel 280 | sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 281 | url: "https://pub.dev" 282 | source: hosted 283 | version: "2.1.2" 284 | string_scanner: 285 | dependency: transitive 286 | description: 287 | name: string_scanner 288 | sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" 289 | url: "https://pub.dev" 290 | source: hosted 291 | version: "1.2.0" 292 | term_glyph: 293 | dependency: transitive 294 | description: 295 | name: term_glyph 296 | sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 297 | url: "https://pub.dev" 298 | source: hosted 299 | version: "1.2.1" 300 | test: 301 | dependency: "direct dev" 302 | description: 303 | name: test 304 | sha256: d983a57c33dde6d44b1fb8635f67c91f4b41d26cf227c147963affa97d63563d 305 | url: "https://pub.dev" 306 | source: hosted 307 | version: "1.24.8" 308 | test_api: 309 | dependency: transitive 310 | description: 311 | name: test_api 312 | sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" 313 | url: "https://pub.dev" 314 | source: hosted 315 | version: "0.6.1" 316 | test_core: 317 | dependency: transitive 318 | description: 319 | name: test_core 320 | sha256: "2f866bf4b20c11327ac166ee6036bddafb7fe9e35505ff8324f788e66913f967" 321 | url: "https://pub.dev" 322 | source: hosted 323 | version: "0.5.8" 324 | tuple: 325 | dependency: "direct main" 326 | description: 327 | name: tuple 328 | sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151 329 | url: "https://pub.dev" 330 | source: hosted 331 | version: "2.0.2" 332 | typed_data: 333 | dependency: transitive 334 | description: 335 | name: typed_data 336 | sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c 337 | url: "https://pub.dev" 338 | source: hosted 339 | version: "1.3.2" 340 | vm_service: 341 | dependency: transitive 342 | description: 343 | name: vm_service 344 | sha256: a13d5503b4facefc515c8c587ce3cf69577a7b064a9f1220e005449cf1f64aad 345 | url: "https://pub.dev" 346 | source: hosted 347 | version: "12.0.0" 348 | watcher: 349 | dependency: transitive 350 | description: 351 | name: watcher 352 | sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" 353 | url: "https://pub.dev" 354 | source: hosted 355 | version: "1.1.0" 356 | web_socket_channel: 357 | dependency: transitive 358 | description: 359 | name: web_socket_channel 360 | sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b 361 | url: "https://pub.dev" 362 | source: hosted 363 | version: "2.4.0" 364 | webkit_inspection_protocol: 365 | dependency: transitive 366 | description: 367 | name: webkit_inspection_protocol 368 | sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" 369 | url: "https://pub.dev" 370 | source: hosted 371 | version: "1.2.1" 372 | yaml: 373 | dependency: transitive 374 | description: 375 | name: yaml 376 | sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" 377 | url: "https://pub.dev" 378 | source: hosted 379 | version: "3.1.2" 380 | sdks: 381 | dart: ">=3.0.0 <4.0.0" 382 | --------------------------------------------------------------------------------