15 Commits

Author SHA1 Message Date
Caijinglong
f8f7b637a3 static method 2019-08-19 15:49:58 +08:00
Caijinglong
88a48be157 修复了一个V1中的bug 2018-08-16 14:26:07 +08:00
cjl_macbook
8bf4ebe233 format code 2018-08-11 21:23:25 +08:00
cjl_macbook
103d92b610 delete some print and update comment 2018-08-11 21:22:16 +08:00
cjl_macbook
6f007e8157 use the string replace's method the resolve the int and double problem.
replace the generate code's num to int.
2018-08-11 17:30:00 +08:00
Caijinglong
65cc06fe99 support version 1.0.0+ 2018-08-10 17:28:46 +08:00
Caijinglong
b507386ccc 更新了当前的版本 修改int 为num类型,以暂时解决dartweb中1.0会被认为是int的问题 2018-08-09 17:58:29 +08:00
Caijinglong
a0b0606c9d update the int to num 2018-08-09 17:27:06 +08:00
Caijinglong
626512e475 fix the need input bug 2018-08-09 16:33:07 +08:00
Caijinglong
bd858b22fb 自动保存entity 名称 2018-08-09 16:30:28 +08:00
Caijinglong
267425767a auto save left input json 2018-08-09 16:23:40 +08:00
Caijinglong
615e39db81 update html add the github 2018-08-09 15:57:19 +08:00
Caijinglong
72303f53fc update LICENSE 2018-08-08 15:08:39 +08:00
Caijinglong
5523e26d77 add the english version 2018-08-08 14:17:00 +08:00
Caijinglong
961628180c 修改了页面的样式 2018-08-08 09:36:50 +08:00
12 changed files with 539 additions and 109 deletions

9
LICENSE Normal file
View File

@@ -0,0 +1,9 @@
Copyright 2018 cjl_spy@163.com
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -1,22 +1,27 @@
# json2dart_serialization
一个网页版本的jsondart
[English readme](https://github.com/CaiJingLong/json2dart/blob/master/README_ENG.md)
一个网页版本的 json 转 dart 库
[直接使用 json2dart](https://caijinglong.github.io/json2dart/index.html)
## 说明
不同于https://github.com/debuggerx01/JSONFormat4Flutter 的是:这个是为了json_serializable创建的
页面较简陋,功能完善😊
感谢[JSONFormat4Flutter](https://github.com/debuggerx01/JSONFormat4Flutter) 给予的灵感
不同于这个库的是:这个是为了 json_serializable 创建的
json_serializable 可以参考flutter官网json部分 [flutter](https://flutter.io/json)
json_serializable 可以参考 flutter 官网 json 部分 [flutter](https://flutter.io/json)
项目的展示地址: https://caijinglong.github.io/json2dart/
项目使用 dart web 开发
项目使用dart web开发
## 说明
主体功能完结,有需要修改的地方欢迎留言
主体功能已完结,有需要修改的地方欢迎留言
## 开发环境
```
其他环境不保证可以编译这份代码
```cli
dart --version
Dart VM version: 2.0.0-dev.69.5 (Tue Jul 31 15:05:14 2018 +0200) on "macos_x64"
```
@@ -26,4 +31,4 @@ Dart VM version: 2.0.0-dev.69.5 (Tue Jul 31 15:05:14 2018 +0200) on "macos_x64"
Created from templates made available by Stagehand under a BSD-style
[license](https://github.com/dart-lang/stagehand/blob/master/LICENSE).
the library also under BSD-style.
the library also under BSD-style.

27
README_ENG.md Normal file
View File

@@ -0,0 +1,27 @@
# json2dart_serialization
a dart web for json2dart
use for web location: [json2dart](https://caijinglong.github.io/json2dart/index.html)
## intro
Thank you for the inspiration of this [JSONFormat4Flutter](https://github.com/debuggerx01/JSONFormat4Flutter)
This project was created for the [json_serializable](https://pub.dartlang.org/packages/json_serializable)
json_serializable also read the flutter's json page [flutter](https://flutter.io/json)
## the code compile env
```cli
dart --version
Dart VM version: 2.0.0-dev.69.5 (Tue Jul 31 15:05:14 2018 +0200) on "macos_x64"
```
## license
Created from templates made available by Stagehand under a BSD-style
[license](https://github.com/dart-lang/stagehand/blob/master/LICENSE).
the library also under BSD-style.

View File

@@ -1,21 +1,39 @@
import 'dart:convert';
import 'package:json2dart_serialization/json_generator.dart';
import 'package:json2dart_serialization/template.dart';
class Generator {
String jsonString;
String entityName;
Version version;
Generator(this.jsonString, [this.entityName]);
Generator(this.jsonString, [this.entityName, this.version = Version.v0]) {
this.jsonString = convertJsonString(jsonString);
}
List<DefaultTemplate> templateList = [];
String makeDartCode() {
var entityName = this.entityName ?? "Entity";
DefaultTemplate template = DefaultTemplate(srcJson: jsonString, className: entityName);
DefaultTemplate template;
if (version == Version.v1) {
template = V1Template(srcJson: jsonString, className: entityName);
} else {
template = DefaultTemplate(srcJson: jsonString, className: entityName);
}
StringBuffer resultSb = StringBuffer();
templateList.add(template);
refreshTemplate(template);
if (!template.isList) {
templateList.add(template);
refreshTemplate(template);
// return resultSb.toString();
} else {
var listTemplate = template.getListTemplate();
templateList.add(listTemplate);
refreshTemplate(template);
}
resultSb.writeln(header);
templateList.forEach((template) {
@@ -28,13 +46,18 @@ class Generator {
var fieldList = template.fieldList;
fieldList.forEach((filed) {
if (filed is MapField) {
// filed.typeString
DefaultTemplate template = DefaultTemplate(srcJson: json.encode(filed.map), className: filed.typeString);
if (version == Version.v1) {
template = V1Template(srcJson: json.encode(filed.map), className: filed.typeString);
}
templateList.add(template);
refreshTemplate(template);
} else if (filed is ListField) {
if (filed.childIsObject) {
DefaultTemplate template = DefaultTemplate(srcJson: json.encode(filed.list[0]), className: filed.typeName);
if (version == Version.v1) {
template = V1Template(srcJson: json.encode(filed.list[0]), className: filed.typeName);
}
templateList.add(template);
refreshTemplate(template);
}
@@ -60,3 +83,22 @@ String camelCase2UnderScoreCase(String name) {
return "_" + str.toLowerCase();
});
}
/// use the string replace's method the resolve the int and double problem.
String convertJsonString(String jsonString) {
var numberReg = RegExp(r"[0-9]\.[0-9]+");
//匹配小数数字正则
var allMatch = numberReg.allMatches(jsonString).toList();
for (var i = 0; i < allMatch.length; i++) {
//是一个小数数字
var m = allMatch[i];
var s = m.group(0);
// 应该是double但由于js的原因被识别成了整数数这里对这种数据进行处理将这里的最后一位从0替换为5以便于让该被js识别成小数 而非数字
s = s.replaceRange(s.length - 1, s.length, "5");
jsonString = jsonString.replaceRange(m.start, m.end, s);
}
return jsonString;
}

View File

@@ -1,44 +1,59 @@
import 'dart:async';
import 'dart:convert';
import 'dart:html';
import 'package:json2dart_serialization/generator.dart';
import 'package:json2dart_serialization/storage.dart';
String entityName = null;
bool useJsonKey = true;
bool isCamelCase = true;
bool isStaticMethod = true;
var downloadFileName = "";
const defaultValue = """{
"body": "",
"data": [1],
"input_content":["1"],
"list1":[{"name":"hello"}],
"number": [1.02],
"user":{"name":"abc"}
}""";
// const defaultValue = """{
// "body": "",
// "data": [1],
// "input_content":["1"],
// "list1":[{"name":"hello"}],
// "number": [1.02],
// "user":{"name":"abc"}
// }""";
const defaultValue = "";
void main() {
enum Version { v0, v1 }
Version v = Version.v0;
void main() async {
isChinese = await _isChinese();
var dataHelper = CookieHelper();
TextAreaElement jsonInput = querySelector("#json");
jsonInput.value = defaultValue;
jsonInput.value = dataHelper.loadJsonString();
jsonInput.onInput.listen((event) {
dataHelper.saveJsonString(jsonInput.value);
refreshData();
});
InputElement entityNameEle = querySelector("#out_entity_name");
entityNameEle.value = dataHelper.loadEntityName();
entityName = entityNameEle.value;
entityNameEle.onInput.listen((event) {
entityName = entityNameEle.value;
dataHelper.saveEntityName(entityName);
refreshData();
});
ButtonElement formatButton = querySelector("#format");
formatButton.onClick.listen((click) {
String pretty;
pretty = convertJsonString(jsonInput.value);
try {
pretty = formatJson(jsonInput.value);
pretty = formatJson(pretty);
} on Exception {
return;
}
@@ -47,7 +62,41 @@ void main() {
InputElement eJsonKey = querySelector("#use_json_key");
InputElement eCamelCase = querySelector("#camelCase");
InputElement eUseStatic = querySelector("#use_static");
TextAreaElement result = querySelector("#result");
RadioButtonInputElement v0 = querySelector("#v0");
RadioButtonInputElement v1 = querySelector("#v1");
void updateVersioin() {
if (v1.checked) {
v = Version.v1;
} else {
v = Version.v0;
}
dataHelper.saveVersion(v);
}
void updateVersionUI() {
if (v == Version.v1) {
v1.checked = true;
} else {
v1.checked = false;
}
}
v = dataHelper.loadVersion();
updateVersionUI();
v0.onInput.listen((event) {
updateVersioin();
refreshData();
});
v1.onInput.listen((event) {
updateVersioin();
refreshData();
});
void onJsonKeyChange() {
useJsonKey = eJsonKey.checked;
@@ -78,6 +127,17 @@ void main() {
refreshData();
});
eUseStatic.checked = isStaticMethod;
eUseStatic.onInput.listen((event) {
isStaticMethod = eUseStatic.checked;
refreshData();
});
querySelector("#useStaticLabel").onClick.listen((event) {
eUseStatic.checked = !eUseStatic.checked;
refreshData();
});
refreshData();
querySelector("#copy").onClick.listen((event) {
@@ -96,7 +156,8 @@ void main() {
// FileWriter fw = await fileEntry.createWriter();
// fw.write(blob);
// File file = await fileEntry.file();
AnchorElement saveLink = document.createElementNS("http://www.w3.org/1999/xhtml", "a");
AnchorElement saveLink =
document.createElementNS("http://www.w3.org/1999/xhtml", "a");
saveLink.href = Url.createObjectUrlFromBlob(blob);
// saveLink.type = "download";
saveLink.download = downloadFileName;
@@ -104,15 +165,40 @@ void main() {
});
}
void refreshData() {
Future<bool> _isChinese() async {
// var lang = await findSystemLocale();
List<MetaElement> elements = querySelectorAll("meta");
String lang;
for (var e in elements) {
var _lang = e.getAttribute("lang");
if (_lang != null) {
lang = _lang;
break;
}
}
if (lang?.contains("zh") == true) {
return true;
}
return false;
}
bool isChinese = false;
void refreshData() async {
TextAreaElement jsonInput = querySelector("#json");
var string = jsonInput.value;
String pretty;
TextAreaElement result = querySelector("#result");
try {
pretty = formatJson(string);
formatJson(string);
} on Exception {
result.value = "不是一个正确的json";
if (isChinese) {
result.value = "不是一个正确的json";
} else {
result.value = "Not JSON";
}
return;
}
String entityClassName;
@@ -122,11 +208,19 @@ void refreshData() {
entityClassName = entityName;
}
var generator = Generator(string, entityClassName);
var generator = Generator(string, entityClassName, v);
var dartCode = generator.makeDartCode();
var dartFileName = ("${generator.fileName}.dart");
downloadFileName = dartFileName;
querySelector("#file_name").text = "应该使用的文件名为: $dartFileName";
String filePrefix;
if (isChinese) {
filePrefix = "应该使用的文件名为:";
} else {
filePrefix = "your dart file name is:";
}
// print(filePrefix);
querySelector("#file_name").text = "$filePrefix $dartFileName";
result.value = dartCode;
}

44
lib/storage.dart Normal file
View File

@@ -0,0 +1,44 @@
import 'dart:html';
import 'package:json2dart_serialization/json_generator.dart';
const _entityKey = "entityKey";
const _versionKey = "versionKey";
class CookieHelper {
String loadJsonString() {
var storage = window.localStorage;
if (!storage.containsKey("json")) {
return "";
}
return window.localStorage["json"];
}
void saveJsonString(String jsonString) {
window.localStorage.addAll({"json": jsonString});
}
void saveEntityName(String entityName) {
window.localStorage.addAll({_entityKey: entityName});
}
String loadEntityName() {
if (!window.localStorage.containsKey(_entityKey)) {
return "";
}
return window.localStorage[_entityKey];
}
void saveVersion(Version version) {
var index = Version.values.indexOf(version);
window.localStorage.addAll({_versionKey: index.toString()});
}
Version loadVersion() {
if (!window.localStorage.containsKey(_versionKey)) {
return Version.v0;
}
return Version.values[int.parse(window.localStorage[_versionKey])];
}
}

View File

@@ -45,7 +45,11 @@ class DefaultTemplate extends Template {
@override
String declare() {
return """@JsonSerializable()
class $className extends Object with _\$${className}SerializerMixin {""";
class $className extends Object ${interface()}{""";
}
String interface() {
return "with _\$${className}SerializerMixin";
}
@override
@@ -93,10 +97,91 @@ class $className extends Object with _\$${className}SerializerMixin {""";
@override
String method() {
if (main.isStaticMethod) {
return " static $className fromJson(Map<String, dynamic> srcJson) => _\$${className}FromJson(srcJson);";
}
return " factory $className.fromJson(Map<String, dynamic> srcJson) => _\$${className}FromJson(srcJson);";
}
List<Field> get fieldList => FieldHelper(srcJson).getFields();
bool get isList => json.decode(srcJson) is List;
ListTemplate getListTemplate() {
if (this is ListTemplate) {
return this;
}
return ListTemplate(
srcJson: srcJson, className: className, delegateTemplate: this);
}
}
class ListTemplate extends DefaultTemplate {
Template delegateTemplate;
ListTemplate(
{String srcJson, String className = "Entity", this.delegateTemplate})
: super(className: className, srcJson: srcJson);
@override
String declare() {
return _declareListMethod() + "\n" + delegateTemplate?.declare() ??
super.declare();
}
String _declareListMethod() {
var listMethod =
"""List<$className> get${className}List(List<dynamic> list){
List<$className> result = [];
list.forEach((item){
result.add($className.fromJson(item));
});
return result;
}""";
return listMethod;
}
@override
String constructor() {
return delegateTemplate?.constructor() ?? super.constructor();
}
@override
String field() {
return delegateTemplate?.field() ?? super.field();
}
@override
String method() {
return delegateTemplate?.method() ?? super.method();
}
@override
String end() {
return delegateTemplate?.end() ?? super.end();
}
@override
List<Field> get fieldList =>
FieldHelper(json.encode(json.decode(srcJson)[0])).getFields();
}
class V1Template extends DefaultTemplate {
V1Template({String srcJson, String className = "Entity"})
: super(className: className, srcJson: srcJson);
@override
String interface() => "";
@override
String method() {
var result = StringBuffer();
result.writeln(super.method());
result.writeln();
result.write(
" Map<String, dynamic> toJson() => _\$${className}ToJson(this);");
return result.toString();
}
}
class FieldHelper {
@@ -104,26 +189,35 @@ class FieldHelper {
FieldHelper(this.srcJson);
List<Field> _getMapFiled(Map<String, dynamic> map) {
List<Field> list = [];
map.forEach((k, v) {
if (v is List) {
list.add(ListField(v, k));
} else if (v is String) {
list.add(SimpleField("String", k));
} else if (v is int) {
list.add(SimpleField("int", k));
} else if (v is double) {
list.add(SimpleField("double", k));
} else if (v is bool) {
list.add(SimpleField("bool", k));
} else if (v is Map<String, dynamic>) {
list.add(MapField(v, k));
}
});
return list;
}
List<Field> getFields() {
var j = json.decode(srcJson);
if (j is Map<String, dynamic>) {
List<Field> list = [];
j.forEach((k, v) {
if (v is List) {
list.add(ListField(v, k));
} else if (v is String) {
list.add(SimpleField("String", k));
} else if (v is int) {
list.add(SimpleField("int", k));
} else if (v is double) {
list.add(SimpleField("double", k));
} else if (v is bool) {
list.add(SimpleField("bool", k));
} else if (v is Map<String, dynamic>) {
list.add(MapField(v, k));
}
});
return list;
return _getMapFiled(j);
} else if (j is List) {
var item = j[0];
if (item is Map<String, dynamic>) {
return _getMapFiled(item);
}
}
return [];
}

View File

@@ -11,9 +11,10 @@ environment:
# path: ^1.4.1
dependencies:
# Your other regular dependencies here
json_annotation: ^0.2.3
json_annotation: ^3.0.0
intl: ^0.15.8
dev_dependencies:
build_runner: ^0.9.0
build_web_compilers: ^0.4.0
json_serializable: ^0.5.4
build_runner: ^1.6.6
build_web_compilers: ^2.2.3
json_serializable: ^3.2.0

BIN
web/github_logo.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

View File

@@ -4,46 +4,66 @@
<head>
<meta charset="utf-8" />
<meta lang="en" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="scaffolded-by" content="https://github.com/google/stagehand" />
<title>hello_world</title>
<title>json2dart_for_json_serializable</title>
<link rel="stylesheet" href="styles.css">
<link rel="icon" href="favicon.ico">
<script defer src="main.dart.js"></script>
</head>
<body>
<div>
<div id="input" class="textarea">
<div class="top">
将json粘贴至左边
</div>
<textarea id="json" title="json字符串"></textarea>
<div>
<button id="format">格式化</button>
</div>
</div>
<div id="output" class="textarea">
<div class="top"></div>
<div class="result_title">
类名称
<input id="out_entity_name" />
<input type="checkbox" id="use_json_key" />
<span id="check_label">jsonKey annotation</span>
<input type="checkbox" id="camelCase" />
<span id="camelCaseLabel">use camelCase</span>
</div>
<div id="file_name"></div>
<textarea id="result" title="结果"></textarea>
<div>
<button id="copy">复制</button>
<button id="save">下载</button>
</div>
</div>
<div class="lang">
<a href="index_ch.html">中文</a>
<a href="https://github.com/caijinglong/json2dart">
<img src="github_logo.jpg" width="50px" />
</a>
</div>
<div>
<h1>simple use
<a href="https://pub.dartlang.org/packages/json_serializable" target="_blank">json_serializable</a>
</h1>
<div class="version">
<input class="version" type="radio" value="0" name="version" id="v0" checked="true">v0.x.x</input>
<input class="version" type="radio" value="1" name="version" id="v1">v1.x.x</input>
</div>
<div>
<div class="title">
<span class="half_span">copy your json to left textarea</span>
<span class="half_span">
<div class="result_title">
dart class name
<input id="out_entity_name" />
<input type="checkbox" id="use_json_key" />
<span id="check_label">jsonKey annotation</span>
<input type="checkbox" id="camelCase" />
<span id="camelCaseLabel">use camelCase</span>
<input type="checkbox" id="use_static" />
<span id="useStaticLabel">use static</span>
</div>
<div id="file_name"></div>
</span>
</div>
<div class="content">
<span class="half_span">
<textarea id="json" title="json" class="content_area"></textarea>
</span>
<span class="half_span">
<textarea id="result" title="result" class="content_area"></textarea>
</span>
</div>
<div class="func">
<span class="half_span">
<button id="format">format</button>
</span>
<span class="half_span">
<button id="copy">copy</button>
<button id="save">download</button>
</span>
</div>
</div>
</body>
</html>

70
web/index_ch.html Normal file
View File

@@ -0,0 +1,70 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta lang="zh-CN" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="scaffolded-by" content="https://github.com/google/stagehand" />
<title>json2dart_for_json_serializable</title>
<link rel="stylesheet" href="styles.css">
<link rel="icon" href="favicon.ico">
<script defer src="main.dart.js"></script>
</head>
<body>
<div class="lang">
<a href="index.html">English</a>
<a href="https://github.com/caijinglong/json2dart">
<img src="github_logo.jpg" width="50px" />
</a>
</div>
<div>
<h1>为了便利使用
<a href="https://pub.dartlang.org/packages/json_serializable">json_serializable</a>
</h1>
<div class="version">
<input class="version" type="radio" value="0" name="version" id="v0" checked="true">v0.x.x</input>
<input class="version" type="radio" value="1" name="version" id="v1">v1.x.x</input>
</div>
</div>
<div>
<div class="title">
<span class="half_span">将json粘贴至左边</span>
<span class="half_span">
<div class="result_title">
类名称
<input id="out_entity_name" />
<input type="checkbox" id="use_json_key" />
<span id="check_label">使用 JsonKey 注解</span>
<input type="checkbox" id="camelCase" />
<span id="camelCaseLabel">驼峰命名</span>
<input type="checkbox" id="use_static" />
<span id="useStaticLabel">使用静态方法</span>
</div>
<div id="file_name"></div>
</span>
</div>
<div class="content">
<span class="half_span">
<textarea id="json" title="json字符串" class="content_area"></textarea>
</span>
<span class="half_span">
<textarea id="result" title="结果" class="content_area"></textarea>
</span>
</div>
<div class="func">
<span class="half_span">
<button id="format">格式化</button>
</span>
<span class="half_span">
<button id="copy">复制</button>
<button id="save">下载</button>
</span>
</div>
</div>
</body>
</html>

View File

@@ -2,40 +2,64 @@
html,
body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
font-family: "Roboto", sans-serif;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
font-family: "Roboto", sans-serif;
}
.top {
height: 4vh;
text-align: center;
padding-top: 1vh;
h1 {
text-align: center;
}
.textarea {
width: 50%;
height: 90%;
text-align: center;
float: left;
.half_span {
width: 50%;
float: left;
text-align: center;
}
textarea {
width: 90%;
height: 80vh;
width: 86%;
height: 70vh;
}
.result_title {
padding-bottom: 15px;
.content {
padding-top: 5vh;
}
#result {
width: 90%;
height: 70vh;
.func .half_span {
height: 7rem;
padding-top: 1rem;
}
.out_entity_name {
height: 40px;
}
button {
width: 6rem;
height: 2rem;
font-size: 1rem;
}
#file_name {
font-size: 0.5rem;
padding-top: 5px;
color: mediumvioletred;
}
.lang {
padding-right: 1vw;
padding-top: 1vh;
text-align: right;
}
a {
color: blue;
text-decoration: none;
}
a:hover {
text-decoration-line: underline;
}
.version {
font-size: 10px;
text-align: center;
}