mirror of
https://github.com/GitJournal/GitJournal.git
synced 2025-08-06 15:21:21 +08:00
353 lines
9.4 KiB
Dart
353 lines
9.4 KiB
Dart
/*
|
|
* SPDX-FileCopyrightText: 2019-2021 Vishesh Handa <me@vhanda.in>
|
|
*
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import 'dart:convert';
|
|
|
|
import 'package:flutter_emoji/flutter_emoji.dart';
|
|
import 'package:yaml/yaml.dart';
|
|
|
|
import 'package:gitjournal/core/folder/notes_folder.dart';
|
|
import 'package:gitjournal/core/folder/notes_folder_fs.dart';
|
|
import 'package:gitjournal/logger/logger.dart';
|
|
import 'package:gitjournal/settings/settings.dart';
|
|
import 'package:gitjournal/utils/datetime.dart';
|
|
import 'file/file.dart';
|
|
import 'md_yaml_doc.dart';
|
|
import 'note.dart';
|
|
|
|
abstract class NoteSerializerInterface {
|
|
void encode(Note note, MdYamlDoc data);
|
|
Note decode({
|
|
required MdYamlDoc data,
|
|
required NotesFolderFS parent,
|
|
required File file,
|
|
});
|
|
}
|
|
|
|
var emojiParser = EmojiParser();
|
|
|
|
enum DateFormat {
|
|
Iso8601,
|
|
UnixTimeStamp,
|
|
None,
|
|
}
|
|
|
|
class NoteSerializationSettings {
|
|
String modifiedKey = "modified";
|
|
String createdKey = "created";
|
|
String titleKey = "title";
|
|
String typeKey = "type";
|
|
String tagsKey = "tags";
|
|
|
|
bool tagsInString = false;
|
|
bool tagsHaveHash = false;
|
|
|
|
SettingsTitle titleSettings = SettingsTitle.Default;
|
|
|
|
var modifiedFormat = DateFormat.Iso8601;
|
|
var createdFormat = DateFormat.Iso8601;
|
|
|
|
var emojify = false;
|
|
|
|
NoteSerializationSettings.fromConfig(NotesFolderConfig config) {
|
|
modifiedKey = config.yamlModifiedKey;
|
|
createdKey = config.yamlCreatedKey;
|
|
tagsKey = config.yamlTagsKey;
|
|
titleSettings = config.titleSettings;
|
|
|
|
// FIXME: modified / created format!
|
|
}
|
|
NoteSerializationSettings();
|
|
|
|
NoteSerializationSettings clone() {
|
|
var s = NoteSerializationSettings();
|
|
s.createdKey = createdKey;
|
|
s.createdFormat = createdFormat;
|
|
s.modifiedKey = modifiedKey;
|
|
s.modifiedFormat = modifiedFormat;
|
|
s.titleKey = titleKey;
|
|
s.typeKey = typeKey;
|
|
s.tagsKey = tagsKey;
|
|
s.tagsInString = tagsInString;
|
|
s.tagsHaveHash = tagsHaveHash;
|
|
s.titleSettings = titleSettings;
|
|
s.emojify = emojify;
|
|
return s;
|
|
}
|
|
}
|
|
|
|
// Rename to MarkdownYamlNoteSerializer
|
|
class NoteSerializer implements NoteSerializerInterface {
|
|
var settings = NoteSerializationSettings();
|
|
|
|
NoteSerializer.fromConfig(this.settings);
|
|
NoteSerializer.raw();
|
|
|
|
@override
|
|
void encode(Note note, MdYamlDoc data) {
|
|
data.body = settings.emojify ? emojiParser.unemojify(note.body) : note.body;
|
|
dynamic _;
|
|
|
|
switch (settings.createdFormat) {
|
|
case DateFormat.Iso8601:
|
|
data.props[settings.createdKey] = toIso8601WithTimezone(note.created);
|
|
break;
|
|
case DateFormat.UnixTimeStamp:
|
|
data.props[settings.createdKey] = toUnixTimeStamp(note.created);
|
|
break;
|
|
case DateFormat.None:
|
|
_ = data.props.remove(settings.createdKey);
|
|
break;
|
|
}
|
|
|
|
switch (settings.modifiedFormat) {
|
|
case DateFormat.Iso8601:
|
|
data.props[settings.modifiedKey] = toIso8601WithTimezone(note.modified);
|
|
break;
|
|
case DateFormat.UnixTimeStamp:
|
|
data.props[settings.modifiedKey] = toUnixTimeStamp(note.modified);
|
|
break;
|
|
case DateFormat.None:
|
|
_ = data.props.remove(settings.modifiedKey);
|
|
break;
|
|
}
|
|
|
|
if (note.title.isNotEmpty) {
|
|
var title = settings.emojify
|
|
? emojiParser.unemojify(note.title.trim())
|
|
: note.title.trim();
|
|
if (settings.titleSettings == SettingsTitle.InH1) {
|
|
if (title.isNotEmpty) {
|
|
data.body = '# $title\n\n${data.body}';
|
|
_ = data.props.remove(settings.titleKey);
|
|
}
|
|
} else {
|
|
if (title.isNotEmpty) {
|
|
data.props[settings.titleKey] = title;
|
|
} else {
|
|
_ = data.props.remove(settings.titleKey);
|
|
}
|
|
}
|
|
} else {
|
|
_ = data.props.remove(settings.titleKey);
|
|
}
|
|
|
|
if (note.type != NoteType.Unknown) {
|
|
var type = note.type.toString().substring(9); // Remove "NoteType."
|
|
data.props[settings.typeKey] = type;
|
|
} else {
|
|
_ = data.props.remove(settings.typeKey);
|
|
}
|
|
|
|
if (note.tags.isEmpty) {
|
|
_ = data.props.remove(settings.tagsKey);
|
|
} else {
|
|
data.props[settings.tagsKey] = note.tags.toList();
|
|
if (settings.tagsInString) {
|
|
var tags = note.tags;
|
|
if (settings.tagsHaveHash) {
|
|
tags = tags.map((e) => '#$e').toSet();
|
|
}
|
|
data.props[settings.tagsKey] = tags.join(' ');
|
|
}
|
|
}
|
|
|
|
note.extraProps.forEach((key, value) {
|
|
data.props[key] = value;
|
|
});
|
|
}
|
|
|
|
static Note decodeNote({
|
|
required MdYamlDoc data,
|
|
required NotesFolderFS parent,
|
|
required File file,
|
|
required NoteSerializationSettings settings,
|
|
}) {
|
|
var serializer = NoteSerializer.fromConfig(settings.clone());
|
|
return serializer.decode(data: data, parent: parent, file: file);
|
|
}
|
|
|
|
@override
|
|
Note decode({
|
|
required MdYamlDoc data,
|
|
required NotesFolderFS parent,
|
|
required File file,
|
|
}) {
|
|
assert(file.filePath.isNotEmpty);
|
|
|
|
var propsUsed = <String>{};
|
|
|
|
DateTime? modified;
|
|
var modifiedKeyOptions = [
|
|
"modified",
|
|
"mod",
|
|
"lastModified",
|
|
"lastMod",
|
|
"lastmodified",
|
|
"lastmod",
|
|
"updated",
|
|
];
|
|
for (var possibleKey in modifiedKeyOptions) {
|
|
var val = data.props[possibleKey];
|
|
if (val != null) {
|
|
if (val is int) {
|
|
modified = parseUnixTimeStamp(val);
|
|
settings.modifiedFormat = DateFormat.UnixTimeStamp;
|
|
} else {
|
|
modified = parseDateTime(val.toString());
|
|
settings.modifiedFormat = DateFormat.Iso8601;
|
|
}
|
|
settings.modifiedKey = possibleKey;
|
|
|
|
var _ = propsUsed.add(possibleKey);
|
|
break;
|
|
}
|
|
}
|
|
if (modified == null) {
|
|
settings.modifiedFormat = DateFormat.None;
|
|
}
|
|
|
|
var body = settings.emojify ? emojiParser.emojify(data.body) : data.body;
|
|
|
|
DateTime? created;
|
|
var createdKeyOptions = [
|
|
"created",
|
|
"date",
|
|
];
|
|
for (var possibleKey in createdKeyOptions) {
|
|
var val = data.props[possibleKey];
|
|
if (val != null) {
|
|
if (val is int) {
|
|
created = parseUnixTimeStamp(val);
|
|
settings.createdFormat = DateFormat.UnixTimeStamp;
|
|
} else {
|
|
created = parseDateTime(val.toString());
|
|
settings.createdFormat = DateFormat.Iso8601;
|
|
}
|
|
settings.createdKey = possibleKey;
|
|
|
|
var _ = propsUsed.add(possibleKey);
|
|
break;
|
|
}
|
|
}
|
|
if (created == null) {
|
|
settings.createdFormat = DateFormat.None;
|
|
}
|
|
|
|
//
|
|
// Title parsing
|
|
//
|
|
String? title;
|
|
if (data.props.containsKey(settings.titleKey)) {
|
|
title = data.props[settings.titleKey]?.toString() ?? "";
|
|
title = settings.emojify ? emojiParser.emojify(title) : title;
|
|
|
|
var _ = propsUsed.add(settings.titleKey);
|
|
settings.titleSettings = SettingsTitle.InYaml;
|
|
} else {
|
|
var startsWithH1 = false;
|
|
for (var line in LineSplitter.split(body)) {
|
|
if (line.trim().isEmpty) {
|
|
continue;
|
|
}
|
|
startsWithH1 = line.startsWith('#') && !line.startsWith('##');
|
|
break;
|
|
}
|
|
|
|
if (startsWithH1) {
|
|
var titleStartIndex = body.indexOf('#');
|
|
var titleEndIndex = body.indexOf('\n', titleStartIndex);
|
|
if (titleEndIndex == -1 || titleEndIndex == body.length) {
|
|
title = body.substring(titleStartIndex + 1).trim();
|
|
body = "";
|
|
} else {
|
|
title = body.substring(titleStartIndex + 1, titleEndIndex).trim();
|
|
body = body.substring(titleEndIndex + 1).trim();
|
|
}
|
|
}
|
|
}
|
|
|
|
NoteType? type;
|
|
var typeStr = data.props[settings.typeKey];
|
|
switch (typeStr) {
|
|
case "Checklist":
|
|
type = NoteType.Checklist;
|
|
break;
|
|
case "Journal":
|
|
type = NoteType.Journal;
|
|
break;
|
|
default:
|
|
type = NoteType.Unknown;
|
|
break;
|
|
}
|
|
if (typeStr != null) {
|
|
var _ = propsUsed.add(settings.typeKey);
|
|
}
|
|
|
|
Set<String>? _tags;
|
|
try {
|
|
var tagKeyOptions = [
|
|
"tags",
|
|
"categories",
|
|
"keywords",
|
|
];
|
|
for (var possibleKey in tagKeyOptions) {
|
|
var tags = data.props[possibleKey];
|
|
if (tags != null) {
|
|
if (tags is YamlList) {
|
|
_tags = tags.map((t) => t.toString()).toSet();
|
|
} else if (tags is List) {
|
|
_tags = tags.map((t) => t.toString()).toSet();
|
|
} else if (tags is String) {
|
|
settings.tagsInString = true;
|
|
var allTags = tags.split(' ');
|
|
settings.tagsHaveHash = allTags.every((t) => t.startsWith('#'));
|
|
if (settings.tagsHaveHash) {
|
|
allTags.removeWhere((e) => e.length <= 1);
|
|
allTags = allTags.map((e) => e.substring(1)).toList();
|
|
}
|
|
|
|
_tags = allTags.toSet();
|
|
} else {
|
|
Log.e("Note Tags Decoding Failed: $tags");
|
|
}
|
|
|
|
settings.tagsKey = possibleKey;
|
|
var _ = propsUsed.add(settings.tagsKey);
|
|
break;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
Log.e("Note Decoding Failed: $e");
|
|
}
|
|
|
|
// Extra Props
|
|
var extraProps = <String, dynamic>{};
|
|
data.props.forEach((key, val) {
|
|
if (propsUsed.contains(key)) {
|
|
return;
|
|
}
|
|
|
|
extraProps[key] = val;
|
|
});
|
|
|
|
return Note.build(
|
|
parent: parent,
|
|
file: file,
|
|
modified: modified,
|
|
created: created,
|
|
body: body,
|
|
title: title ?? "",
|
|
noteType: type,
|
|
extraProps: extraProps,
|
|
tags: _tags ?? {},
|
|
doc: data,
|
|
serializerSettings: settings,
|
|
fileFormat: NoteFileFormat.Markdown,
|
|
);
|
|
}
|
|
}
|