diff --git a/.vscode/settings.json b/.vscode/settings.json index adc491b1a5..a3323f8287 100755 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,14 +6,21 @@ "editor.defaultFormatter": "dbaeumer.vscode-eslint" }, "cSpell.words": [ + "commonmark", "contenteditable", + "execall", "frontmatter", "Häusler", "hfelix", "Jocs", + "katex", + "laynezh", "marktext", "muya", + "Setext", "snabbdom", - "Tkaixiang" + "snapsvg", + "Tkaixiang", + "webfontloader" ] } diff --git a/packages/desktop/src/common/i18n.ts b/packages/desktop/src/common/i18n.ts index c313709daa..561b2da720 100644 --- a/packages/desktop/src/common/i18n.ts +++ b/packages/desktop/src/common/i18n.ts @@ -3,7 +3,7 @@ import path from 'path' export type Translations = Record -const SUPPORTED_LANGUAGES = ['en', 'zh-CN', 'zh-TW', 'es', 'fr', 'de', 'ja', 'ko', 'pt'] as const +const SUPPORTED_LANGUAGES = ['en', 'zh-CN', 'zh-TW', 'es', 'fr', 'de', 'ja', 'ko', 'pt', 'tr'] as const export type SupportedLanguage = (typeof SUPPORTED_LANGUAGES)[number] diff --git a/packages/desktop/src/main/preferences/schema.json b/packages/desktop/src/main/preferences/schema.json index cc09368add..fadc3f0ea7 100644 --- a/packages/desktop/src/main/preferences/schema.json +++ b/packages/desktop/src/main/preferences/schema.json @@ -282,6 +282,12 @@ "enum": ["hand", "simple"], "default": "hand" }, + "plantumlServer": { + "description": "Markdown--PlantUML server URL for rendering diagrams", + "type": "string", + "pattern": "^(https?://|$)", + "default": "https://www.plantuml.com/plantuml" + }, "theme": { "description": "Theme--Select the theme used in MarkText", "type": "string", diff --git a/packages/desktop/src/renderer/src/components/editorWithTabs/editor.vue b/packages/desktop/src/renderer/src/components/editorWithTabs/editor.vue index d804c38fe2..3b8f9495a8 100644 --- a/packages/desktop/src/renderer/src/components/editorWithTabs/editor.vue +++ b/packages/desktop/src/renderer/src/components/editorWithTabs/editor.vue @@ -113,6 +113,7 @@ import { ja, ko, pt, + tr, zhCN, zhTW, type ILocale @@ -157,6 +158,7 @@ const MUYA_LOCALES: Record = { ja, ko, pt, + tr, 'zh-CN': zhCN, 'zh-TW': zhTW } @@ -584,6 +586,12 @@ watch(sequenceTheme, (value, oldValue) => { } }) +watch(() => preferencesStore.plantumlServer, (value, oldValue) => { + if (value !== oldValue && editor.value) { + editor.value.setOptions({ plantumlServer: value }, true) + } +}) + watch(listIndentation, (value, oldValue) => { if (value !== oldValue && editor.value) { editor.value.setListIndentation(value) @@ -1450,6 +1458,13 @@ const handleFileChange = (payload: unknown) => { if (savedEngineHistory) { editor.value.setHistory(savedEngineHistory) } + // First activation of a tab the save-tracking allocator has never seen: + // seed its clean baseline from the engine's serialization now, before + // any edit. For a tab that already has a tracker this is a no-op — + // switching back must keep the existing content -> id map. + if (id) { + getSyntheticHistory(id, editor.value.getMarkdown()) + } } } else if (newCursor) { editor.value.setCursor(newCursor) @@ -1578,6 +1593,7 @@ onMounted(() => { hideLinkPopup: hideLinkPopup.value, autoCheck: autoCheck.value, sequenceTheme: sequenceTheme.value, + plantumlServer: preferencesStore.plantumlServer, spellcheckEnabled: spellcheckerEnabled.value, // Resolve the OS clipboard to a local file path on paste (image-from-file). clipboardFilePath: guessClipboardFilePath, @@ -1613,6 +1629,15 @@ onMounted(() => { muya.init() editor.value = muya + // Seed the save-tracking baseline for the mount-loaded document (from the + // engine's OWN serialization, same reason as setMarkdownToEditor). Without + // this the allocator is created lazily on the first `json-change` — i.e. + // after the first edit — so the pristine content never maps to id 0 and + // undoing back to the on-disk content can never read as clean again (PG15). + if (currentFile.value?.id) { + getSyntheticHistory(currentFile.value.id, muya.getMarkdown()) + } + const container = getScrollContainer()! // Listen for language changes and update the engine locale. diff --git a/packages/desktop/src/renderer/src/components/exportSettings/index.vue b/packages/desktop/src/renderer/src/components/exportSettings/index.vue index 4a768b0a4a..b28e87fa56 100644 --- a/packages/desktop/src/renderer/src/components/exportSettings/index.vue +++ b/packages/desktop/src/renderer/src/components/exportSettings/index.vue @@ -163,7 +163,7 @@ [] => [ { label: t('preferences.general.misc.language.portuguese'), value: 'pt' + }, + { + label: t('preferences.general.misc.language.turkish'), + value: 'tr' } ] diff --git a/packages/desktop/src/renderer/src/prefComponents/image/components/folderSetting/index.vue b/packages/desktop/src/renderer/src/prefComponents/image/components/folderSetting/index.vue index a8ea2335dc..656a3582eb 100644 --- a/packages/desktop/src/renderer/src/prefComponents/image/components/folderSetting/index.vue +++ b/packages/desktop/src/renderer/src/prefComponents/image/components/folderSetting/index.vue @@ -28,7 +28,7 @@ @@ -129,6 +135,7 @@ import { usePreferencesStore } from '@/store/preferences' import type { PreferencesState } from '@/store/preferences' import Bool from '../common/bool/index.vue' import CurSelect from '../common/select/index.vue' +import TextBox from '../common/textBox/index.vue' import { bulletListMarkerOptions, orderListDelimiterOptions, @@ -154,7 +161,8 @@ const { footnote, isHtmlEnabled, isGitlabCompatibilityEnabled, - sequenceTheme + sequenceTheme, + plantumlServer } = storeToRefs(preferenceStore) const onSelectChange = (type: keyof PreferencesState, value: unknown): void => { diff --git a/packages/desktop/src/renderer/src/store/preferences.ts b/packages/desktop/src/renderer/src/store/preferences.ts index 5f03983991..c5fdcb8fed 100644 --- a/packages/desktop/src/renderer/src/store/preferences.ts +++ b/packages/desktop/src/renderer/src/store/preferences.ts @@ -80,6 +80,7 @@ export interface PreferencesState { isHtmlEnabled: boolean isGitlabCompatibilityEnabled: boolean sequenceTheme: SequenceTheme | string + plantumlServer: string // ----- Theme ----- theme: string @@ -195,6 +196,7 @@ export const usePreferencesStore = defineStore('preferences', { isHtmlEnabled: true, isGitlabCompatibilityEnabled: false, sequenceTheme: 'hand', + plantumlServer: 'https://www.plantuml.com/plantuml', theme: 'light', followSystemTheme: true, diff --git a/packages/desktop/src/types/muya-core.d.ts b/packages/desktop/src/types/muya-core.d.ts index b30cb5f757..5c7ccb0d3a 100644 --- a/packages/desktop/src/types/muya-core.d.ts +++ b/packages/desktop/src/types/muya-core.d.ts @@ -31,6 +31,7 @@ declare module '@muyajs/core' { export const ja: ILocale export const ko: ILocale export const pt: ILocale + export const tr: ILocale export const zhCN: ILocale export const zhTW: ILocale diff --git a/packages/desktop/static/locales/de.json b/packages/desktop/static/locales/de.json index f80b380dd1..01ca4c959d 100644 --- a/packages/desktop/static/locales/de.json +++ b/packages/desktop/static/locales/de.json @@ -454,6 +454,7 @@ "isHtmlEnabled": "HTML-Rendering aktivieren", "isGitlabCompatibilityEnabled": "GitLab-Kompatibilitätsmodus aktivieren", "sequenceTheme": "Sequenzdiagramm-Theme", + "plantumlServer": "PlantUML-Server-URL", "theme": "Das in MarkText verwendete Theme auswählen", "followSystemTheme": "Systemdesign folgen", "lightModeTheme": "Design, das verwendet werden soll, wenn sich das System im hellen Modus befindet", @@ -551,7 +552,8 @@ "german": "Deutsch", "japanese": "日本語", "korean": "한국어", - "portuguese": "Português" + "portuguese": "Português", + "turkish": "Türkçe" } } }, @@ -770,6 +772,9 @@ "title": "Sequenz-Design", "handDrawn": "Handgezeichnet", "simple": "Einfach" + }, + "plantumlServer": { + "title": "PlantUML-Server-URL" } }, "math": { diff --git a/packages/desktop/static/locales/en.json b/packages/desktop/static/locales/en.json index cf011cb5c8..57eb6ea4ff 100644 --- a/packages/desktop/static/locales/en.json +++ b/packages/desktop/static/locales/en.json @@ -453,6 +453,7 @@ "isHtmlEnabled": "Enable HTML rendering", "isGitlabCompatibilityEnabled": "Enable GitLab compatibility mode", "sequenceTheme": "Sequence diagram theme", + "plantumlServer": "PlantUML server URL", "theme": "Select the theme used in MarkText", "followSystemTheme": "Follow System Theme", "lightModeTheme": "Theme to use when system is in light mode", @@ -550,7 +551,8 @@ "german": "Deutsch", "japanese": "日本語", "korean": "한국어", - "portuguese": "Português" + "portuguese": "Português", + "turkish": "Türkçe" } } }, @@ -769,6 +771,9 @@ "title": "Sequence Theme", "handDrawn": "Hand Drawn", "simple": "Simple" + }, + "plantumlServer": { + "title": "PlantUML Server URL" } }, "math": { diff --git a/packages/desktop/static/locales/es.json b/packages/desktop/static/locales/es.json index cd95960e03..f53bb54650 100644 --- a/packages/desktop/static/locales/es.json +++ b/packages/desktop/static/locales/es.json @@ -454,6 +454,7 @@ "isHtmlEnabled": "Habilitar renderizado HTML", "isGitlabCompatibilityEnabled": "Habilitar modo de compatibilidad GitLab", "sequenceTheme": "Tema del diagrama de secuencia", + "plantumlServer": "URL del servidor PlantUML", "theme": "Seleccionar el tema usado en MarkText", "followSystemTheme": "Seguir tema del sistema", "lightModeTheme": "Tema a usar cuando el sistema está en modo claro", @@ -551,7 +552,8 @@ "german": "Deutsch", "japanese": "日本語", "korean": "한국어", - "portuguese": "Português" + "portuguese": "Português", + "turkish": "Türkçe" } } }, @@ -770,6 +772,9 @@ "title": "Tema de Secuencia", "handDrawn": "Dibujado a Mano", "simple": "Simple" + }, + "plantumlServer": { + "title": "URL del servidor PlantUML" } }, "math": { diff --git a/packages/desktop/static/locales/fr.json b/packages/desktop/static/locales/fr.json index 8b712f11a9..fd9a6cb5a2 100644 --- a/packages/desktop/static/locales/fr.json +++ b/packages/desktop/static/locales/fr.json @@ -455,6 +455,7 @@ "isHtmlEnabled": "Activer le rendu HTML", "isGitlabCompatibilityEnabled": "Activer le mode de compatibilité GitLab", "sequenceTheme": "Thème du diagramme de séquence", + "plantumlServer": "URL du serveur PlantUML", "theme": "Sélectionner le thème utilisé dans MarkText", "followSystemTheme": "Suivre le thème du système", "lightModeTheme": "Thème à utiliser lorsque le système est en mode clair", @@ -552,7 +553,8 @@ "german": "Deutsch", "japanese": "日本語", "korean": "한국어", - "portuguese": "Português" + "portuguese": "Português", + "turkish": "Türkçe" } } }, @@ -772,6 +774,9 @@ "title": "Thème de séquence", "handDrawn": "Dessiné à la main", "simple": "Simple" + }, + "plantumlServer": { + "title": "URL du serveur PlantUML" } }, "math": { diff --git a/packages/desktop/static/locales/ja.json b/packages/desktop/static/locales/ja.json index 129e192286..38cd92752a 100644 --- a/packages/desktop/static/locales/ja.json +++ b/packages/desktop/static/locales/ja.json @@ -455,6 +455,7 @@ "isHtmlEnabled": "HTMLレンダリングを有効にする", "isGitlabCompatibilityEnabled": "GitLab互換モードを有効にする", "sequenceTheme": "シーケンス図のテーマ", + "plantumlServer": "PlantUMLサーバーのURL", "theme": "MarkTextで使用するテーマを選択", "followSystemTheme": "システムのテーマに従う", "lightModeTheme": "システムがライトモードの時に使用するテーマ", @@ -552,7 +553,8 @@ "german": "Deutsch", "japanese": "日本語", "korean": "한국어", - "portuguese": "Português" + "portuguese": "Português", + "turkish": "Türkçe" } } }, @@ -772,6 +774,9 @@ "title": "シーケンステーマ", "handDrawn": "手描き", "simple": "シンプル" + }, + "plantumlServer": { + "title": "PlantUMLサーバーのURL" } }, "math": { diff --git a/packages/desktop/static/locales/ko.json b/packages/desktop/static/locales/ko.json index 254fe40f82..9d925cd18f 100644 --- a/packages/desktop/static/locales/ko.json +++ b/packages/desktop/static/locales/ko.json @@ -455,6 +455,7 @@ "isHtmlEnabled": "HTML 렌더링 활성화", "isGitlabCompatibilityEnabled": "GitLab 호환 모드 활성화", "sequenceTheme": "시퀀스 다이어그램 테마", + "plantumlServer": "PlantUML 서버 URL", "theme": "MarkText에서 사용할 테마 선택", "followSystemTheme": "시스템 테마 따르기", "lightModeTheme": "시스템이 라이트 모드일 때 사용할 테마", @@ -552,7 +553,8 @@ "german": "Deutsch", "japanese": "日本語", "korean": "한국어", - "portuguese": "Português" + "portuguese": "Português", + "turkish": "Türkçe" } } }, @@ -772,6 +774,9 @@ "title": "시퀀스 테마", "handDrawn": "손으로 그린", "simple": "간단" + }, + "plantumlServer": { + "title": "PlantUML 서버 URL" } }, "math": { diff --git a/packages/desktop/static/locales/pt.json b/packages/desktop/static/locales/pt.json index 81b4ddc604..bc9eee8750 100644 --- a/packages/desktop/static/locales/pt.json +++ b/packages/desktop/static/locales/pt.json @@ -455,6 +455,7 @@ "isHtmlEnabled": "Habilitar renderização HTML", "isGitlabCompatibilityEnabled": "Habilitar modo de compatibilidade GitLab", "sequenceTheme": "Tema do diagrama de sequência", + "plantumlServer": "URL do servidor PlantUML", "theme": "Selecionar o tema usado no MarkText", "followSystemTheme": "Seguir tema do sistema", "lightModeTheme": "Tema a ser usado quando o sistema está no modo claro", @@ -552,7 +553,8 @@ "german": "Deutsch", "japanese": "日本語", "korean": "한국어", - "portuguese": "Português" + "portuguese": "Português", + "turkish": "Türkçe" } } }, @@ -772,6 +774,9 @@ "title": "Tema de Sequência", "handDrawn": "Desenhado à Mão", "simple": "Simples" + }, + "plantumlServer": { + "title": "URL do servidor PlantUML" } }, "math": { diff --git a/packages/desktop/static/locales/tr.json b/packages/desktop/static/locales/tr.json new file mode 100644 index 0000000000..8b983faf29 --- /dev/null +++ b/packages/desktop/static/locales/tr.json @@ -0,0 +1,1398 @@ +{ + "menu": { + "counter": { + "words": "Sözcükler", + "characters": "Karakterler", + "paragraphs": "Paragraflar" + }, + "file": { + "file": "Dosya", + "new": "Yeni", + "newTab": "Yeni Sekme", + "newWindow": "Yeni Pencere", + "open": "Aç", + "openFile": "Dosya Aç...", + "openFolder": "Klasör Aç...", + "openRecent": "Son Kullanılanları Aç", + "clearRecentlyUsed": "Son Kullanılanları Temizle", + "save": "Kaydet", + "saveAs": "Farklı Kaydet...", + "autoSave": "Otomatik Kaydet", + "moveTo": "Taşı", + "rename": "Yeniden Adlandır", + "export": "Dışa Aktar", + "exportHtml": "HTML Olarak Dışa Aktar", + "exportPdf": "PDF Olarak Dışa Aktar", + "import": "İçe Aktar", + "print": "Yazdır", + "recent": "Son Dosyalar", + "close": "Kapat", + "closeAll": "Tümünü Kapat", + "closeTab": "Sekmeyi Kapat", + "closeWindow": "Pencereyi Kapat", + "quit": "Çıkış", + "preferences": "Tercihler" + }, + "edit": { + "edit": "Düzen", + "undo": "Geri Al", + "redo": "Yinele", + "cut": "Kes", + "copy": "Kopyala", + "paste": "Yapıştır", + "copyAsRich": "Zengin Metin Olarak Kopyala", + "copyAsHtml": "HTML Olarak Kopyala", + "pasteAsPlainText": "Düz Metin Olarak Yapıştır", + "selectAll": "Tümünü Seç", + "duplicate": "Çoğalt", + "createParagraph": "Paragraf Oluştur", + "deleteParagraph": "Paragrafı Sil", + "find": "Bul", + "replace": "Değiştir", + "findNext": "Sonrakini Bul", + "findPrevious": "Öncekini Bul", + "findInFolder": "Klasörde Bul", + "screenshot": "Ekran Görüntüsü", + "lineEnding": "Satır Sonu", + "lineEndingCrlf": "CRLF(Windows)", + "lineEndingLf": "LF(Linux/Mac)" + }, + "paragraph": { + "title": "Paragraf", + "paragraph": "Paragraf", + "heading1": "Başlık 1", + "heading2": "Başlık 2", + "heading3": "Başlık 3", + "heading4": "Başlık 4", + "heading5": "Başlık 5", + "heading6": "Başlık 6", + "promoteHeading": "Başlığı Yükselt", + "demoteHeading": "Başlığı Alçalt", + "table": "Tablo", + "codeFences": "Kod Blokları", + "codeBlock": "Kod Bloğu", + "quoteBlock": "Alıntı Bloğu", + "mathBlock": "Matematik Bloğu", + "htmlBlock": "HTML Bloğu", + "quote": "Alıntı", + "orderedList": "Sıralı Liste", + "bulletList": "Sırasız Liste", + "unorderedList": "Sırasız Liste", + "taskList": "Görev Listesi", + "looseListItem": "Gevşek Liste Öğesi", + "paragraphItem": "Paragraf Öğesi", + "horizontalRule": "Yatay Çizgi", + "frontMatter": "Ön Bilgi" + }, + "window": { + "title": "Pencere", + "window": "Pencere", + "minimize": "Simge Durumuna Küçült", + "close": "Kapat", + "zoom": "Yakınlaştırma", + "zoomIn": "Yakınlaştır", + "zoomOut": "Uzaklaştır", + "fullScreen": "Tam Ekran", + "bringAllToFront": "Tümünü Öne Getir", + "alwaysOnTop": "Her Zaman Üstte" + }, + "help": { + "help": "Yardım", + "markdownReference": "Markdown Başvurusu", + "changelog": "Değişiklik Günlüğü", + "askQuestion": "Soru Sor", + "reportBug": "Hata Bildir", + "viewSource": "Kaynağı Görüntüle", + "followUs": "Bizi Takip Edin", + "support": "MarkText'e Destek Ol", + "license": "Lisans", + "checkForUpdates": "Güncellemeleri Denetle", + "aboutMarkText": "MarkText Hakkında", + "about": "Hakkında", + "checkUpdates": "Güncellemeleri Denetle" + }, + "marktext": { + "title": "MarkText", + "marktext": "MarkText", + "about": "MarkText Hakkında", + "checkUpdates": "Güncellemeleri Denetle...", + "preferences": "Tercihler", + "services": "Hizmetler", + "hide": "MarkText'i Gizle", + "hideMarkText": "MarkText'i Gizle", + "hideOthers": "Diğerlerini Gizle", + "showAll": "Tümünü Göster", + "quit": "MarkText'ten Çık" + }, + "view": { + "view": "Görünüm", + "toggleSidebar": "Kenar Çubuğunu Aç/Kapat", + "toggleTabbar": "Sekme Çubuğunu Aç/Kapat", + "commandPalette": "Komut Paleti", + "sourceCode": "Kaynak Kod Modu", + "sourceCodeMode": "Kaynak Kod Modu", + "typewriter": "Daktilo Modu", + "typewriterMode": "Daktilo Modu", + "focus": "Odak Modu", + "focusMode": "Odak Modu", + "showTabBar": "Sekme Çubuğunu Göster", + "toc": "İçindekiler", + "toggleTableOfContents": "İçindekileri Aç/Kapat", + "reloadImages": "Görselleri Yeniden Yükle", + "showDeveloperTools": "Geliştirici Araçlarını Göster", + "reloadWindow": "Pencereyi Yeniden Yükle" + }, + "format": { + "format": "Biçim", + "bold": "Kalın", + "italic": "İtalik", + "underline": "Altı Çizili", + "strikethrough": "Üstü Çizili", + "code": "Satır İçi Kod", + "codeBlock": "Kod Bloğu", + "quote": "Alıntı", + "unorderedList": "Sırasız Liste", + "orderedList": "Sıralı Liste", + "taskList": "Görev Listesi", + "table": "Tablo", + "link": "Bağlantı", + "image": "Görsel", + "heading1": "Başlık 1", + "heading2": "Başlık 2", + "heading3": "Başlık 3", + "heading4": "Başlık 4", + "heading5": "Başlık 5", + "heading6": "Başlık 6", + "superscript": "Üst Simge", + "subscript": "Alt Simge", + "highlight": "Vurgu", + "inlineCode": "Satır İçi Kod", + "inlineMath": "Satır İçi Matematik", + "hyperlink": "Köprü", + "clearFormatting": "Biçimlendirmeyi Temizle", + "clearFormat": "Biçimlendirmeyi Temizle" + }, + "theme": { + "theme": "&Tema", + "light": "Açık", + "dark": "Koyu", + "auto": "Otomatik", + "followThemDisabled": "Sistem temasını takip ederken tema seçimi devre dışı", + "lightThemes": "— Açık Temalar —", + "darkThemes": "— Koyu Temalar —", + "ayuDark": "Ayu Dark", + "ayuLight": "Ayu Light", + "ayuMirage": "Ayu Mirage", + "cadmiumDark": "Cadmium Dark", + "cadmiumLight": "Cadmium Light", + "catppuccinLatte": "Catppuccin Latte", + "catppuccinMocha": "Catppuccin Mocha", + "cyberdream": "cyberdream", + "dracula": "Dracula", + "everforestDark": "Everforest Dark", + "everforestLight": "Everforest Light", + "graphiteLight": "Graphite Light", + "gruvboxDark": "Gruvbox Dark", + "gruvboxLight": "Gruvbox Light", + "horizonDark": "Horizon Dark", + "kanagawa": "Kanagawa", + "materialDark": "Material Dark", + "monokaiPro": "Monokai Pro", + "nightfox": "Nightfox", + "nord": "Nord", + "oneDark": "One Dark", + "oxocarbonDark": "Oxocarbon Dark", + "palenight": "Palenight", + "rosePine": "Rosé Pine", + "rosePineDawn": "Rosé Pine Dawn", + "rosePineMoon": "Rosé Pine Moon", + "solarizedDark": "Solarized Dark", + "solarizedLight": "Solarized Light", + "synthwave84": "Synthwave '84", + "tokyoNight": "Tokyo Night", + "tokyoNightLight": "Tokyo Night Light", + "tokyoNightStorm": "Tokyo Night Storm", + "ulyssesLight": "Ulysses Light" + } + }, + "contextMenu": { + "cut": "Kes", + "copy": "Kopyala", + "paste": "Yapıştır", + "copyAsRich": "Zengin Metin Olarak Kopyala", + "copyAsHtml": "HTML Olarak Kopyala", + "pasteAsPlainText": "Düz Metin Olarak Yapıştır", + "insertParagraphBefore": "Önüne Paragraf Ekle", + "insertParagraphAfter": "Arkasına Paragraf Ekle", + "spelling": "Yazım...", + "changeLanguage": "Dili Değiştir...", + "addToDictionary": "Sözlüğe Ekle", + "editDictionary": "Sözlüğü Düzenle...", + "sideBar": { + "newFile": "Yeni Dosya", + "newDirectory": "Yeni Klasör", + "copy": "Kopyala", + "cut": "Kes", + "paste": "Yapıştır", + "rename": "Yeniden Adlandır", + "moveToTrash": "Çöp Kutusuna Taşı", + "showInFolder": "Klasörde Göster" + }, + "tabs": { + "close": "Kapat", + "closeOthers": "Diğerlerini kapat", + "closeSavedTabs": "Kaydedilmiş sekmeleri kapat", + "closeAllTabs": "Tüm sekmeleri kapat", + "rename": "Yeniden Adlandır", + "copyPath": "Yolu kopyala", + "showInFolder": "Klasörde göster" + } + }, + "search": { + "searchPlaceholder": "Ara", + "caseSensitive": "Büyük/Küçük Harf Duyarlı", + "wholeWord": "Tam sözcük seç", + "useRegex": "Sorguyu RegEx olarak kullan", + "replacementPlaceholder": "Değiştirme", + "replaceAll": "Tümünü Değiştir", + "replaceSingle": "Tek Değiştir", + "invalidRegex": "Geçersiz düzenli ifade \"{pattern}\"", + "regexMatchEmpty": "Düzenli ifade boş dizeyle eşleşiyor \"{pattern}\"", + "searchResultInfo": "{fileCount} dosyada {matchCount} eşleşme", + "searchLimited": "Arama ilk {count} sonuçla sınırlandırıldı" + }, + "sideBar": { + "search": { + "searchInFolder": "Klasörde ara...", + "noFolderOpen": "Açık klasör yok", + "noResultsFound": "Sonuç bulunamadı.", + "cancel": "İptal", + "openFolder": "Klasör Aç", + "showMoreMatches": "Daha fazla eşleşme göster" + }, + "tree": { + "openedFiles": "Açık dosyalar", + "saveAll": "Tümünü Kaydet", + "closeAll": "Tümünü Kapat", + "emptyProject": "Boş proje", + "createFile": "Dosya Oluştur", + "openFolder": "Klasör Aç" + }, + "toc": { + "title": "İçindekiler" + }, + "icons": { + "files": "Dosyalar", + "search": "Ara", + "toc": "İçindekiler", + "settings": "Ayarlar" + } + }, + "commandPalette": { + "placeholder": "Çalıştırmak için bir komut yazın", + "placeholders": { + "selectOption": "Bir seçenek seçin", + "searchFileToOpen": "Açılacak dosyayı arayın", + "selectLanguage": "Geçilecek dili seçin" + } + }, + "exportSettings": { + "autoNumberingHeadings": "Başlıkları otomatik numaralandır", + "title": "Dışa Aktarma Seçenekleri", + "info": { + "label": "Bilgi", + "description": "Lütfen sayfa görünümünü özelleştirin ve devam etmek için \"dışa aktar\"a tıklayın." + }, + "page": { + "label": "Sayfa", + "pageTitle": "Sayfa başlığı:", + "pageSize": "Sayfa boyutu:", + "landscapeOrientation": "Yatay yönlendirme:", + "pageMargin": "mm cinsinden sayfa kenar boşluğu:", + "topBottom": "Üst/Alt:", + "leftRight": "Sol/Sağ:", + "widthHeight": "mm cinsinden Genişlik/Yükseklik:" + }, + "showFrontMatter": "Ön bilgiyi göster", + "style": { + "label": "Stil", + "overwriteThemeFont": "Tema yazı tipi ayarlarının üzerine yaz", + "fontFamily": "Yazı tipi ailesi:", + "fontSize": "Yazı tipi boyutu", + "lineHeight": "Satır yüksekliği" + }, + "theme": { + "label": "Tema", + "description": "Bir tema seçerek veya el yapımı bir tema oluşturarak belge görünümünü değiştirebilirsiniz.", + "theme": "Tema" + }, + "headerFooter": { + "allowStyled": "Stilli metne izin ver", + "customizeStyle": "Stili özelleştir", + "fontSize": "Yazı tipi boyutu", + "footerType": "Alt bilgi türü", + "headerType": "Üst bilgi türü", + "label": "Üst Bilgi ve Alt Bilgi", + "leftFooterText": "Sol alt bilgi metni", + "leftHeaderText": "Sol üst bilgi metni", + "mainFooterText": "Ana alt bilgi metni", + "mainHeaderText": "Ana üst bilgi metni", + "rightFooterText": "Sağ alt bilgi metni", + "rightHeaderText": "Sağ üst bilgi metni", + "description": "Üst bilgi ve/veya alt bilgi tanımlanmışsa metin tüm sayfalarda görünür." + }, + "toc": { + "includeTopHeading": "En üst başlığı dahil et", + "includeTopHeadingDetail": "En üst başlık ayrıntısını dahil et", + "label": "İçindekiler", + "title": "İçindekiler" + }, + "export": "Dışa Aktar...", + "options": { + "pageSizes": { + "a3": "A3 (297mm x 420mm)", + "a4": "A4 (210mm x 297mm)", + "a5": "A5 (148mm x 210mm)", + "legal": "US Legal (8.5\" x 13\")", + "letter": "US Letter (8.5\" x 11\")", + "tabloid": "Tabloid (17\" x 11\")", + "custom": "Özel" + }, + "headerFooterTypes": { + "none": "Yok", + "singleCell": "Tek hücre", + "threeCells": "Üç hücre" + }, + "headerFooterStyles": { + "default": "Varsayılan", + "simple": "Basit", + "styled": "Stilli" + }, + "themes": { + "academic": "Akademik", + "default": "GitHub (Varsayılan)", + "liber": "Liber" + } + } + }, + "table": { + "resizeTable": "Tabloyu Yeniden Boyutlandır", + "alignLeft": "Sola Hizala", + "alignCenter": "Ortala", + "alignRight": "Sağa Hizala", + "deleteTable": "Tabloyu Sil" + }, + "recent": { + "noTabsOpen": "Açık sekmeniz yok.", + "newFile": "Yeni Dosya" + }, + "import": { + "title": "İçe Aktar veya Aç", + "description": "İçeriklerinizi MarkText'e almak için buraya bırakın" + }, + "preferences": { + "title": "Tercihler", + "search": { + "placeholder": "Tercihlerde ara...", + "optionalValues": "isteğe bağlı değerler", + "categories": { + "general": "Genel", + "editor": "Düzenleyici", + "markdown": "Markdown", + "theme": "Tema", + "image": "Görsel", + "view": "Görünüm", + "searcher": "Arayıcı", + "watcher": "İzleyici", + "spelling": "Yazım", + "custom css": "Özel CSS" + }, + "items": { + "autoSave": "Düzenlenen içeriği otomatik olarak kaydet", + "autoSaveDelay": "Bir değişiklikten sonra dosyanın kaydedileceği süre (ms)", + "titleBarStyle": "Başlık çubuğu stili (yalnızca Windows ve Linux)", + "openFilesInNewWindow": "Dosyaları yeni pencerede aç", + "openFolderInNewWindow": "Klasörü menü aracılığıyla yeni pencerede aç", + "zoom": "Yakınlaştırma düzeyi. 0.5 ile 2.0 arasında (dahil)", + "hideScrollbar": "Kaydırma çubuklarının gizlenip gizlenmeyeceği", + "wordWrapInToc": "İçindekilerde sözcük kaydırmanın etkinleştirilip etkinleştirilmeyeceği", + "fileSortBy": "Açık klasördeki dosyaları oluşturulma zamanı, değiştirilme zamanı ve başlığa göre sırala", + "startUpAction": "MarkText başladıktan sonraki eylem: son düzenlenen içeriği aç, belirtilen klasörü aç veya boş sayfa", + "restoreLayoutState": "Başlangıçta önceki düzenleyici durumunu geri yükle", + "defaultDirectoryToOpen": "startUp=folder olduğunda başlangıçta açılması gereken varsayılan klasör", + "language": "MarkText'in kullandığı dil", + "editorFontFamily": "Düzenleyici yazı tipi ailesi", + "fontSize": "Piksel cinsinden yazı tipi boyutu", + "lineHeight": "Satır Yüksekliği", + "wrapCodeBlocks": "Kod bloklarındaki metni kaydır", + "editorLineWidth": "Maksimum düzenleyici alanı genişliğini tanımlar. Boş bir dize veya ch (karakter), px (piksel) ya da % (yüzde) son ekleri kullanılabilir", + "codeFontSize": "Kod bloğundaki yazı tipi boyutu, aralık 12 ~ 18", + "codeFontFamily": "Kod bloğunda kullanılan yazı tipi ailesi", + "codeBlockLineNumbers": "Satır numaralarının gösterilip gösterilmeyeceği", + "trimUnnecessaryCodeBlockEmptyLines": "Kod bloğundaki baştaki ve sondaki boş satırları kırp", + "autoPairBracket": "Düzenlerken parantezleri otomatik tamamla", + "autoPairMarkdownSyntax": "Markdown söz dizimini otomatik tamamla", + "autoPairQuote": "Tırnak işaretlerini otomatik tamamla", + "endOfLine": "Her satırın sonunda kullanılan yeni satır karakteri. Varsayılan değer, işletim sisteminizin varsayılan yeni satır karakterini seçen 'default' değeridir", + "defaultEncoding": "Varsayılan dosya kodlaması", + "autoGuessEncoding": "Dosyaları açarken dosya kodlamasını otomatik olarak tahmin etmeyi dene", + "trimTrailingNewline": "Sondaki yeni satırı kırpma seçeneği", + "textDirection": "Metin yazım yönü", + "hideQuickInsertHint": "Paragrafları hızlı oluşturma ipucunu gizle", + "hideLinkPopup": "İmleç bağlantının üzerine geldiğinde bağlantı açılır penceresini gizle", + "autoCheck": "İlgili görevin otomatik olarak işaretlenip işaretlenmeyeceği", + "preferLooseListItem": "Tercih edilen liste türü", + "bulletListMarker": "Sırasız listede kullanılan işaret", + "orderListDelimiter": "Sıralı listede kullanılan ayraç", + "preferHeadingStyle": "MarkText'te tercih edilen başlık stili", + "tabSize": "Sekmeyi x boşlukla değiştir", + "listIndentation": "Liste girintisini seçin", + "frontmatterType": "Ön bilgi türü", + "superSubScript": "Pandoc'un markdown uzantısı üst simge ve alt simgeyi etkinleştir", + "footnote": "Pandoc'un markdown uzantısı dipnotu etkinleştir", + "isHtmlEnabled": "HTML işlemeyi etkinleştir", + "isGitlabCompatibilityEnabled": "GitLab uyumluluk modunu etkinleştir", + "sequenceTheme": "Dizi diyagramı teması", + "theme": "MarkText'te kullanılan temayı seçin", + "followSystemTheme": "Sistem Temasını Takip Et", + "lightModeTheme": "Sistem açık moddayken kullanılacak tema", + "darkModeTheme": "Sistem koyu moddayken kullanılacak tema", + "customCss": "Geçerli temaya uygulanacak özel CSS", + "spellcheckerEnabled": "Yazım denetiminin etkinleştirilip etkinleştirilmeyeceği", + "spellcheckerNoUnderline": "Yazım hatalarının altını çizme", + "spellcheckerLanguage": "Yazım denetleyici dili", + "imageInsertAction": "Yerel klasörden görsel ekledikten sonraki varsayılan davranış", + "imagePreferRelativeDirectory": "Göreli görsel klasörünün tercih edilip edilmeyeceği", + "imageRelativeDirectoryBase": "Göreli görsellerin nereye kopyalanacağı", + "imageRelativeDirectoryName": "Göreli görsel klasörünün adı", + "sideBarVisibility": "Kenar çubuğunun görünür olup olmadığı", + "tabBarVisibility": "Sekmelerin gösterilip gösterilmediği", + "sourceCodeModeEnabled": "Kaynak kod modunun varsayılan olarak etkin olup olmadığı", + "searchExclusions": "Aramadan hariç tutulacak glob desenleri listesi", + "searchMaxFileSize": "Maksimum dosya boyutu (). K, M veya G son ekleri kullanılabilir; son ek verilmezse sayı bayt olarak kabul edilir", + "searchIncludeHidden": "Gizli dosya ve klasörlerde arama yapılıp yapılmayacağı", + "searchNoIgnore": ".gitignore gibi yoksayma dosyalarının yoksayılıp yoksayılmayacağı", + "searchFollowSymlinks": "Sembolik bağlantıların izlenip izlenmeyeceği", + "watcherUsePolling": "Yoklama kullanılıp kullanılmayacağı. Yoklama yüksek CPU kullanımına yol açabilir ancak ağ üzerindeki dosyaları izlemek için gereklidir" + } + }, + "categories": { + "general": "Genel", + "editor": "Düzenleyici", + "markdown": "Markdown", + "spelling": "Yazım", + "theme": "Tema", + "image": "Görsel", + "keybindings": "Kısayol Tuşları" + }, + "general": { + "title": "Genel", + "autoSave": { + "title": "Otomatik Kaydet", + "description": "Otomatik kaydet", + "delayDescription": "Otomatik kaydetme gecikmesi" + }, + "window": { + "title": "Pencere", + "titleBarStyle": { + "title": "Başlık Çubuğu Stili", + "custom": "Özel", + "titleBarStyle": "Başlık Çubuğu Stili", + "native": "Yerel" + }, + "requiresRestart": "Yeniden başlatma gerektirir", + "hideScrollbars": "Kaydırma çubuklarını gizle", + "openFilesInNewWindow": "Dosyaları yeni pencerede aç", + "openFoldersInNewWindow": "Klasörleri yeni pencerede aç", + "zoom": "Yakınlaştırma" + }, + "startup": { + "title": "Başlangıç Seçenekleri", + "layoutOptions": "Düzen Seçenekleri", + "startupFilesFolders": "Başlangıç Dosyaları/Klasörleri", + "restorePreviousState": "Önceki düzenleyici durumunu geri yükle", + "openBlankState": "Boş durum aç", + "openDefaultDirectory": "Varsayılan klasörü aç", + "openLastFolder": "Açık klasörü geri yükle", + "restoreAll": "Tüm açık dosya ve klasörleri geri yükle", + "selectFolder": "Klasör Seç", + "openBlankPage": "Boş sayfa aç" + }, + "sidebar": { + "title": "Kenar Çubuğu", + "wrapTextInToc": "İçindekilerde metni kaydır", + "showOpenedFiles": "Açık dosyaları göster", + "excludePatterns": "Hariç tutma desenleri", + "excludePatternsNotes": "Hariç tutma desenleri (Notlar)", + "fileSortBy": { + "title": "Dosya Sıralama Ölçütü", + "creationTime": "Oluşturulma Zamanı", + "modificationTime": "Değiştirilme Zamanı", + "filename": "Dosya Adı" + }, + "fileSortOrder": { + "title": "Sıralama Düzeni", + "aToZ": "A → Z", + "zToA": "Z → A", + "oldestFirst": "Önce En Eski", + "newestFirst": "Önce En Yeni" + } + }, + "misc": { + "title": "Çeşitli", + "language": { + "title": "Dil", + "english": "English", + "chinese": "简体中文", + "traditionalChinese": "繁體中文", + "spanish": "Español", + "french": "Français", + "german": "Deutsch", + "japanese": "日本語", + "korean": "한국어", + "portuguese": "Português", + "turkish": "Türkçe" + } + } + }, + "editor": { + "title": "Düzenleyici", + "textEditor": { + "title": "Metin Düzenleyici", + "fontSize": "Yazı tipi boyutu", + "lineHeight": "Satır yüksekliği", + "fontFamily": "Yazı tipi ailesi", + "maxWidth": "Maksimum genişlik", + "maxWidthNotes": "Düzenleyici içeriğinin maksimum genişliği" + }, + "codeBlock": { + "title": "Kod Bloğu", + "fontSize": "Yazı tipi boyutu", + "fontFamily": "Yazı tipi ailesi", + "showLineNumbers": "Satır numaralarını göster", + "removeEmptyLines": "Boş satırları kaldır" + }, + "writingBehavior": { + "title": "Yazma Davranışı", + "autoCloseBrackets": "Parantezleri otomatik kapat", + "autoCompleteMarkdown": "Markdown'u otomatik tamamla", + "autoCloseQuotes": "Tırnakları otomatik kapat" + }, + "fileRepresentation": { + "title": "Dosya Gösterimi", + "tabWidth": "Sekme genişliği", + "lineSeparator": "Satır ayırıcı", + "defaultEncoding": "Varsayılan kodlama", + "autoDetectEncoding": "Kodlamayı otomatik algıla", + "trailingNewlines": { + "title": "Sondaki Yeni Satırlar", + "doNothing": "Hiçbir Şey Yapma", + "ensureOne": "Bir Tane Garanti Et", + "preserve": "Koru", + "trimAll": "Tümünü Kırp" + }, + "endOfLine": { + "default": "Varsayılan", + "crlf": "CRLF(Windows)", + "lf": "LF(Linux/Mac)" + } + }, + "misc": { + "title": "Çeşitli", + "textDirection": { + "title": "Metin Yönü", + "ltr": "Soldan Sağa", + "rtl": "Sağdan Sola" + }, + "hideQuickInsertHint": "Hızlı ekleme ipucunu gizle", + "hideLinkPopup": "Bağlantı açılır penceresini gizle", + "autoCheck": "Otomatik işaretle", + "autoNormalizeLineEndings": "Açılışta satır sonlarını LF'ye otomatik normalleştir", + "wrapCodeBlocks": "Kod bloklarını kaydır" + } + }, + "image": { + "title": "Görsel", + "actions": { + "upload": "Buluta yükle", + "folder": "Klasöre kopyala", + "path": "Mutlak yolu kullan" + }, + "folderSetting": { + "title": "Klasör Ayarı", + "globalFolder": "Genel görsel klasörü", + "open": "Aç", + "showInFolder": "Klasörde Göster", + "preferRelative": "Göreli klasörü tercih et", + "relativeCopyLocation": "Görseli şuna göre kopyala", + "copyRelativeToFile": "Dosyaya göre", + "copyRelativeToFolder": "Klasöre göre", + "relativeFolderName": "Göreli klasör adı", + "filenameNote": "Dosya adı otomatik olarak oluşturulacak." + }, + "imageInsertAction": { + "title": "Görsel ekleme eylemi", + "folder": "Klasöre yükle", + "path": "Yola kopyala", + "upload": "Buluta yükle", + "copyToFolder": "Klasöre kopyala", + "uploadToServer": "Sunucuya yükle" + }, + "imageFolderPath": { + "title": "Görsel klasörü yolu", + "description": "Görsel klasörü yolu", + "selectFolder": "Klasör seç", + "relativePath": "Göreli yol", + "absolutePath": "Mutlak yol" + }, + "cloudPictureBed": { + "title": "Bulut görsel deposu", + "github": "GitHub", + "smms": "SM.MS", + "qiniu": "Qiniu", + "upyun": "UpYun", + "tcyun": "Tencent Cloud", + "aliyun": "Alibaba Cloud", + "imgur": "Imgur", + "picgo": "PicGo" + }, + "preferRelativeDirectory": { + "title": "Göreli klasörü tercih et", + "description": "Görseller için göreli klasörü tercih et" + }, + "uploader": { + "title": "Görsel Yükleyici", + "currentUploader": "Geçerli Yükleyici: {name}", + "picgoNotInstalled": "PicGo yüklü değil", + "picgoDetectionStatus": "PicGo Algılama Durumu", + "picgoInstalled": "PicGo yüklü", + "picgoDetectionFailed": "PicGo algılaması başarısız", + "debugInfo": "Hata Ayıklama Bilgisi", + "installCommand": "Yükleme Komutu", + "npmInstallCommand": "npm install picgo -g", + "yarnInstallCommand": "yarn global add picgo", + "pnpmInstallCommand": "pnpm add -g picgo", + "chooseInstallMethod": "Yükleme yöntemini seçin:", + "retestPicgo": "Yeniden Test Et", + "detecting": "Algılanıyor", + "lastDetectionTime": "Son Algılama Zamanı", + "detectionStatus": "Algılama Durumu", + "picgoDetection": "PicGo Algılama", + "usageGuide": { + "title": "PicGo Temel Kullanım Kılavuzu", + "step1": "Yükleyiciyi Yapılandır", + "step1Description": "PicGo yükleyicisini yapılandırmak için aşağıdaki komutu kullanın:", + "step2": "Görselleri Yükle", + "step2Description": "Görselleri yüklemek için aşağıdaki komutu kullanın:", + "step3": "Yapılandırmayı Görüntüle", + "step3Description": "Geçerli yapılandırmayı görüntülemek için aşağıdaki komutu kullanın:", + "documentation": "Tam belgeleri görüntüle" + }, + "autoDetection": "Otomatik algılanıyor", + "lastSuccessTime": "Son başarı zamanı", + "neverDetected": "Hiç algılanmadı", + "neverSuccessful": "Hiç başarılı olmadı", + "configureUploader": "Yükleyiciyi Yapılandır", + "configureDescription": "PicGo yükleyicisini yapılandırmak için aşağıdaki komutu kullanın:", + "uploadImage": "Görselleri Yükle", + "uploadDescription": "Görselleri yüklemek için aşağıdaki komutu kullanın:", + "viewConfig": "Yapılandırmayı Görüntüle", + "viewConfigDescription": "Geçerli yapılandırmayı görüntülemek için aşağıdaki komutu kullanın:", + "viewDocumentation": "Tam belgeleri görüntüle", + "scriptDescription": "Betik Açıklaması", + "scriptLocation": "Betik Konumu", + "save": "Kaydet", + "saveConfig": "Yapılandırmayı Kaydet", + "scriptConfigSaved": "Betik yapılandırması kaydedildi", + "services": { + "picgo": "PicGo", + "cliScript": "CLI Betiği" + }, + "pleaseInstall": "Lütfen yükleyin", + "scriptPath": "Betik yolu" + }, + "defaultBehavior": "Görseli MarkText'e yapıştırdıktan veya sürükledikten sonraki varsayılan davranış" + }, + "markdown": { + "title": "Markdown", + "listItem": { + "title": "Liste Öğesi", + "preferLooseListItem": "Gevşek liste öğesini tercih et", + "bulletListMarker": "Sırasız liste işareti", + "orderListDelimiter": "Sıralı liste ayracı", + "listIndentation": { + "title": "Liste Girintisi", + "dfm": "Varsayılan Biçim Modu", + "tab": "Sekme", + "oneSpace": "Bir Boşluk", + "twoSpaces": "İki Boşluk", + "threeSpaces": "Üç Boşluk", + "fourSpaces": "Dört Boşluk" + } + }, + "heading": { + "title": "Başlık", + "preferHeadingStyle": "Başlık stilini tercih et" + }, + "frontmatter": { + "title": "Ön Bilgi", + "frontmatterType": "Ön bilgi türü" + }, + "extensions": { + "title": "Uzantılar", + "superSubScript": "Üst/Alt Simge", + "footnote": "Dipnot", + "isHtmlEnabled": "HTML etkin", + "isGitlabCompatibilityEnabled": "GitLab uyumluluğu", + "sequenceTheme": "Dizi teması", + "superscript": "Üst Simge", + "subscript": "Alt Simge", + "frontMatter": "Ön Bilgi", + "math": "Matematik", + "diagram": "Diyagram", + "footnoteNotes": "Dipnot notları", + "frontmatterType": { + "frontmatterType": "Ön Bilgi Türü", + "jsonBrace": "Süslü Parantezli JSON", + "jsonSemicolon": "Noktalı Virgüllü JSON", + "title": "Ön Bilgi Türü" + } + }, + "diagrams": { + "title": "Diyagramlar", + "plantuml": "PlantUML", + "mermaid": "Mermaid", + "vega": "Vega-Lite", + "flowchart": "Akış Şeması", + "sequence": "Dizi", + "gantt": "Gantt", + "sequenceTheme": { + "title": "Dizi Teması", + "handDrawn": "El Çizimi", + "simple": "Basit" + } + }, + "math": { + "title": "Matematik", + "katex": "KaTeX", + "mathJax": "MathJax" + }, + "misc": { + "preferHeadingStyle": { + "title": "Tercih Edilen Başlık Stili", + "atx": "ATX Stili", + "setext": "Setext Stili" + }, + "title": "Çeşitli" + }, + "compatibility": { + "enableGitlab": "GitLab uyumluluğunu etkinleştir", + "enableHtml": "HTML desteğini etkinleştir", + "title": "Uyumluluk" + }, + "lists": { + "bulletListMarker": "Sırasız liste işareti", + "listIndentation": { + "title": "Liste Girintisi", + "dfm": "DFM Stili", + "fourSpaces": "Dört Boşluk", + "oneSpace": "Bir Boşluk", + "tab": "Sekme", + "threeSpaces": "Üç Boşluk", + "twoSpaces": "İki Boşluk" + }, + "orderListDelimiter": "Sıralı liste ayracı", + "preferLooseListItem": "Gevşek liste öğelerini tercih et", + "title": "Listeler" + } + }, + "spelling": { + "title": "Yazım", + "enabled": "Yazım denetleyiciyi etkinleştir", + "autoDetectLanguage": "Dili otomatik algıla", + "language": "Dil", + "noSuggestions": "Öneri yok", + "spellcheckerEnabled": "Yazım denetleyiciyi etkinleştir", + "spellcheckerNoUnderline": "Yanlış yazılan sözcüklerin altını çizme", + "spellcheckerLanguage": "Yazım denetleyici dili" + }, + "theme": { + "title": "Tema", + "theme": "Tema", + "followSystemTheme": "Sistem Temasını Takip Et", + "modeThemes": "Tema Seçimi", + "lightModeTheme": "Açık mod teması", + "darkModeTheme": "Koyu mod teması", + "customCss": "Özel CSS", + "codeBlockTheme": { + "title": "Kod bloğu teması", + "description": "Kod blokları için tema" + }, + "importCustomThemes": "Özel temaları içe aktar", + "importTheme": "Temayı İçe Aktar", + "openFolder": "Klasör Aç", + "openThemesFolder": "Temalar klasörünü aç" + }, + "keybindings": { + "title": "Kısayol Tuşları", + "description": "Klavye kısayollarını özelleştir", + "searchPlaceholder": "Kısayol tuşlarında ara", + "command": "Komut", + "keybinding": "Kısayol", + "reset": "Sıfırla", + "resetAll": "Tümünü sıfırla", + "edit": "Düzenle", + "cancel": "İptal", + "save": "Kaydet", + "restoreDefaults": "Varsayılanları geri yükle", + "debugOptions": "Hata Ayıklama Seçenekleri", + "dumpKeyboardInfo": "Klavye Bilgilerini Dök", + "failedToSave": "Kısayol tuşları kaydedilemedi", + "keyInputDialog": { + "instructions": "Atamak istediğiniz tuş kombinasyonuna basın", + "invalidKeybinding": "Geçersiz tuş kombinasyonu", + "placeholder": "Tuşlara basın..." + }, + "online": "Çevrimiçi", + "saveError": "Kısayol tuşları kaydedilirken hata", + "shortcutInUse": "Kısayol kullanımda", + "shortcutInUseMessage": "Bu kısayol zaten {accelerator} öğesine atanmış", + "table": { + "description": "Açıklama", + "edit": "Düzenle", + "keyCombination": "Tuş Kombinasyonu", + "options": "Seçenekler", + "reset": "Sıfırla", + "unbind": "Bağlantıyı Kaldır" + } + }, + "selectFont": "Yazı Tipi Seç", + "spellchecker": { + "autoDetectDescription": "Belge dilini otomatik olarak algıla", + "autoDetectLanguage": "Dili otomatik algıla", + "customDictionary": { + "delete": "Sil", + "description": "Kişisel sözlüğünüze özel sözcükler ekleyin", + "noWordsAvailable": "Sözcük yok", + "options": "Seçenekler", + "title": "Özel Sözlük", + "word": "Sözcük" + }, + "defaultLanguage": "Varsayılan dil", + "enableSpellChecking": "Yazım denetimini etkinleştir", + "hideMarksForErrors": "Yazım hataları için işaretleri gizle", + "title": "Yazım Denetleyici" + } + }, + "edit": { + "undo": "Geri Al", + "redo": "Yinele", + "cut": "Kes", + "copy": "Kopyala", + "paste": "Yapıştır", + "selectAll": "Tümünü Seç" + }, + "about": { + "copyright": "Telif Hakkı © Luo Ran 2017-{year}", + "copyrightContributors": "Github Katkıda Bulunanlar tarafından <3 ile yapıldı" + }, + "commands": { + "mt": { + "hide": "MarkText'i Gizle", + "hideOthers": "Diğerlerini Gizle" + }, + "file": { + "newWindow": "Yeni Pencere", + "newTab": "Yeni Sekme", + "openFile": "Dosya Aç", + "openFolder": "Klasör Aç", + "save": "Kaydet", + "saveAs": "Farklı Kaydet", + "moveFile": "Dosyayı Taşı", + "renameFile": "Dosyayı Yeniden Adlandır", + "quickOpen": "Hızlı Aç", + "changeEncoding": "Kodlamayı Değiştir", + "changeLineEnding": "Satır Sonunu Değiştir", + "trailingNewline": "Sondaki Yeni Satır", + "print": "Yazdır", + "preferences": "Tercihler", + "closeTab": "Sekmeyi Kapat", + "closeWindow": "Pencereyi Kapat", + "quit": "Çıkış", + "toggleAutoSave": "Otomatik Kaydetmeyi Aç/Kapat", + "importFile": "Dosya İçe Aktar", + "exportFile": "Dosya Dışa Aktar", + "exportFilePdf": "PDF Olarak Dışa Aktar", + "zoom": "Yakınlaştırma", + "checkUpdate": "Güncellemeleri Denetle", + "lineEnding": "Satır Sonu", + "close": "Kapat" + }, + "edit": { + "undo": "Geri Al", + "redo": "Yinele", + "cut": "Kes", + "copy": "Kopyala", + "paste": "Yapıştır", + "copyAsRich": "Zengin Metin Olarak Kopyala", + "copyAsHtml": "HTML Olarak Kopyala", + "pasteAsPlaintext": "Düz Metin Olarak Yapıştır", + "selectAll": "Tümünü Seç", + "duplicate": "Çoğalt", + "createParagraph": "Paragraf Oluştur", + "deleteParagraph": "Paragrafı Sil", + "find": "Bul", + "findNext": "Sonrakini Bul", + "findPrevious": "Öncekini Bul", + "replace": "Değiştir", + "findInFolder": "Klasörde Bul", + "screenshot": "Ekran Görüntüsü", + "mathBlock": "Matematik Bloğu" + }, + "paragraph": { + "heading1": "Başlık 1", + "heading2": "Başlık 2", + "heading3": "Başlık 3", + "heading4": "Başlık 4", + "heading5": "Başlık 5", + "heading6": "Başlık 6", + "upgradeHeading": "Başlığı Yükselt", + "degradeHeading": "Başlığı Alçalt", + "table": "Tablo", + "codeFence": "Kod Bloğu", + "quoteBlock": "Alıntı Bloğu", + "mathFormula": "Matematik Formülü", + "htmlBlock": "HTML Bloğu", + "orderList": "Sıralı Liste", + "bulletList": "Sırasız Liste", + "taskList": "Görev Listesi", + "looseListItem": "Gevşek Liste Öğesi", + "paragraph": "Paragraf", + "horizontalLine": "Yatay Çizgi", + "frontMatter": "Ön Bilgi", + "resetParagraph": "Paragrafı Sıfırla", + "mathBlock": "Matematik Bloğu", + "horizontalRule": "Yatay Çizgi" + }, + "format": { + "strong": "Kalın", + "emphasis": "İtalik", + "underline": "Altı Çizili", + "superscript": "Üst Simge", + "subscript": "Alt Simge", + "highlight": "Vurgu", + "inlineCode": "Satır İçi Kod", + "inlineMath": "Satır İçi Matematik", + "strike": "Üstü Çizili", + "hyperlink": "Köprü", + "image": "Görsel", + "clearFormat": "Biçimlendirmeyi Temizle" + }, + "window": { + "minimize": "Simge Durumuna Küçült", + "close": "Kapat", + "toggleAlwaysOnTop": "Her Zaman Üstte'yi Aç/Kapat", + "zoomIn": "Yakınlaştır", + "zoomOut": "Uzaklaştır", + "toggleFullScreen": "Tam Ekranı Aç/Kapat", + "changeTheme": "Temayı Değiştir" + }, + "view": { + "commandPalette": "Komut Paleti", + "sourceCodeMode": "Kaynak Kod Modu", + "typewriterMode": "Daktilo Modu", + "focusMode": "Odak Modu", + "toggleSourceCodeMode": "Kaynak Kod Modunu Aç/Kapat", + "toggleTypewriterMode": "Daktilo Modunu Aç/Kapat", + "toggleFocusMode": "Odak Modunu Aç/Kapat", + "toggleSidebar": "Kenar Çubuğunu Aç/Kapat", + "toggleToc": "İçindekileri Aç/Kapat", + "toggleTabbar": "Sekme Çubuğunu Aç/Kapat", + "toggleDevTools": "Geliştirici Araçlarını Aç/Kapat", + "devReload": "Geliştirici Yeniden Yükleme", + "reloadImages": "Görselleri Yeniden Yükle", + "textDirection": "Metin Yönü", + "actualSize": "Gerçek Boyut", + "zoomIn": "Yakınlaştır", + "zoomOut": "Uzaklaştır", + "devToggleDeveloperTools": "Geliştirici Araçlarını Aç/Kapat" + }, + "tabs": { + "cycleForward": "İleri Geç", + "cycleBackward": "Geri Geç", + "switchToLeft": "Sola Geç", + "switchToRight": "Sağa Geç", + "switchToFirst": "Birinciye Geç", + "switchToSecond": "İkinciye Geç", + "switchToThird": "Üçüncüye Geç", + "switchToFourth": "Dördüncüye Geç", + "switchToFifth": "Beşinciye Geç", + "switchToSixth": "Altıncıya Geç", + "switchToSeventh": "Yedinciye Geç", + "switchToEighth": "Sekizinciye Geç", + "switchToNinth": "Dokuzuncuya Geç", + "switchToTenth": "Onuncuya Geç" + }, + "docs": { + "userGuide": "Kullanıcı Kılavuzu", + "markdownSyntax": "Markdown Söz Dizimi" + }, + "utils": { + "noUpdateResourceFile": "Güncelleme kaynak dosyası yok", + "todoUpdateCheck": "Güncellemeleri denetle" + }, + "spellchecker": { + "switchLanguage": "Dili Değiştir" + } + }, + "common": { + "cancel": "İptal", + "ok": "Tamam" + }, + "dialog": { + "cancel": "İptal", + "changesWillBeLost": "Değişiklikler kaybolacak", + "close": "Kapat", + "dontSave": "Kaydetme", + "file": "Dosya", + "fileExists": "Dosya zaten var: {filename}", + "files": "Dosyalar", + "importWarning": "İçe aktarma uyarısı", + "installPandoc": "Pandoc'u Yükle", + "keepOpen": "Açık tut", + "replace": "Değiştir", + "save": "Kaydet", + "saveChanges": "Değişiklikleri kaydet", + "saveFailure": "Kaydetme hatası" + }, + "error": { + "configSchemaViolation": "Yapılandırma şeması ihlali", + "copyError": "Kopyalama hatası", + "initializationFailed": "Başlatma başarısız {hint}", + "otherInstanceDetected": "Başka bir MarkText penceresi zaten açık", + "report": "Bildir", + "startupError": "Başlangıç hatası", + "terminatedDueToError": "Hata nedeniyle sonlandırıldı", + "unexpectedErrorWithMessage": "Beklenmeyen hata: {message}", + "unexpectedMainProcess": "Beklenmeyen ana işlem hatası", + "unexpectedRendererProcess": "Beklenmeyen işleyici işlem hatası" + }, + "notifications": { + "defaultMessage": "Varsayılan mesaj", + "defaultTitle": "Bildirim" + }, + "store": { + "editor": { + "anchorLinkCopied": "Çapa bağlantısı kopyalandı", + "tabNotFound": "Sekme bulunamadı", + "tocItemNotFound": "İçindekiler {key} bulunamadı", + "highlightStart": "[highlight start]", + "highlightEnd": "[highlight end]", + "typeAtToInsert": "Eklemek için / yazın", + "inputFootnoteDefinition": "Dipnot tanımını girin...", + "inputYamlFrontMatter": "YAML Ön Bilgisini girin", + "inputLanguageIdentifier": "Dil Tanımlayıcısını girin...", + "inputMathematicalFormula": "Matematiksel Formülü girin...", + "fence": "kod bloğu", + "indent": "girinti", + "frontMatterDelimiter": "---", + "mathDelimiter": "$$", + "mermaidStart": "``` mermaid", + "flowchartStart": "``` flowchart", + "sequenceStart": "``` sequence", + "plantumlStart": "``` plantuml", + "vegaLiteStart": "``` vega-lite", + "codeFence": "```", + "clickToAddImage": "Görsel eklemek için tıklayın", + "loadImageFailed": "Görsel yüklenemedi", + "errorLoadingTabTitle": "Sekme yüklenirken hata", + "errorLoadingTabMessage": "Sekme bulunamadığı için dosya değişikliği yüklenirken bir hata oluştu.", + "mixedLineEndingsNormalized": "\"{name}\" karışık satır sonları içeriyor; otomatik olarak {lineEnding} biçimine normalleştirildi.", + "imageDeletionUrlTitle": "Görsel silme URL'si", + "imageDeletionUrlMessage": "Yüklenen görselin silme URL'sini panoya kopyalamak için tıklayın {url}", + "errorWhileSaving": "Kaydedilirken bir hata oluştu: {msg}", + "exportSuccessTitle": "Başarıyla dışa aktarıldı", + "exportSuccessMessage": "\"{name}\" başarıyla dışa aktarıldı!", + "fileRemovedOnDisk": "\"{name}\" diskten kaldırıldı.", + "fileChangedOnDisk": "\"{name}\" dosyasında değişiklikler var, yeniden yüklensin mi?" + } + }, + "spellchecker": { + "failedToRemoveWord": "Sözcük sözlükten kaldırılamadı", + "unexpectedError": "Beklenmeyen yazım denetleyici hatası" + }, + "quickInsert": { + "basicBlock": "Temel Blok", + "header": "Başlık", + "advancedBlock": "Gelişmiş Blok", + "listBlock": "Liste Bloğu", + "diagram": "Diyagram", + "paragraph": { + "title": "Paragraf", + "subtitle": "Metin içeriği girin" + }, + "horizontalLine": { + "title": "Yatay Çizgi", + "subtitle": "---" + }, + "frontMatter": { + "title": "Ön Bilgi", + "subtitle": "--- YAML ---" + }, + "header1": { + "title": "Başlık 1", + "subtitle": "# Başlık" + }, + "header2": { + "title": "Başlık 2", + "subtitle": "## Başlık" + }, + "header3": { + "title": "Başlık 3", + "subtitle": "### Başlık" + }, + "header4": { + "title": "Başlık 4", + "subtitle": "#### Başlık" + }, + "header5": { + "title": "Başlık 5", + "subtitle": "##### Başlık" + }, + "header6": { + "title": "Başlık 6", + "subtitle": "###### Başlık" + }, + "tableBlock": { + "title": "Tablo Bloğu", + "subtitle": "| Başlık | Başlık |" + }, + "mathFormula": { + "title": "Matematik Formülü", + "subtitle": "$$ Formül $$" + }, + "htmlBlock": { + "title": "HTML Bloğu", + "subtitle": "
HTML
" + }, + "codeBlock": { + "title": "Kod Bloğu", + "subtitle": "``` Kod ```" + }, + "quoteBlock": { + "title": "Alıntı Bloğu", + "subtitle": "> Alıntı içeriği" + }, + "orderedList": { + "title": "Sıralı Liste", + "subtitle": "1. Liste öğesi" + }, + "bulletList": { + "title": "Sırasız Liste", + "subtitle": "- Liste öğesi" + }, + "todoList": { + "title": "Liste", + "subtitle": "- [ ] Görev öğesi" + }, + "vegaChart": { + "title": "Vega-Lite Grafiği", + "subtitle": "Veri görselleştirme grafiği" + }, + "flowChart": { + "title": "Akış Şeması", + "subtitle": "Akış şeması diyagramı" + }, + "sequenceChart": { + "title": "Dizi Grafiği", + "subtitle": "Dizi diyagramı" + }, + "plantUMLChart": { + "title": "PlantUML Grafiği", + "subtitle": "UML diyagramı" + }, + "mermaid": { + "title": "Mermaid Grafiği", + "subtitle": "Mermaid diyagramı", + "gantt": { + "title": "Gantt Grafiği" + }, + "pie": { + "title": "Pasta Grafiği" + }, + "flowchart": { + "title": "Akış Şeması" + }, + "sequence": { + "title": "Dizi Diyagramı" + }, + "class": { + "title": "Sınıf Diyagramı" + }, + "state": { + "title": "Durum Diyagramı", + "subtitle": "stateDiagram" + }, + "journey": { + "title": "Kullanıcı Yolculuğu" + }, + "git": { + "title": "Git Grafiği" + }, + "er": { + "title": "ER Diyagramı" + }, + "requirement": { + "title": "Gereksinim Diyagramı" + } + }, + "taskList": { + "title": "Görev Listesi" + }, + "vegaliteChart": { + "title": "Vega-Lite Grafiği" + }, + "plantUMLDiagram": { + "title": "PlantUML Diyagramı" + }, + "mermaidDiagram": { + "title": "Mermaid Diyagramı" + } + }, + "frontMenu": { + "duplicate": "Çoğalt", + "turnInto": "Dönüştür", + "newParagraph": "Yeni Paragraf", + "delete": "Sil", + "paragraph": "Paragraf", + "table": "Tablo", + "html": "HTML", + "mathblock": "Matematik Bloğu", + "pre": "Kod Bloğu", + "frontMatter": "Ön Bilgi", + "ulTask": "Görev Listesi", + "ulBullet": "Sırasız Liste", + "olOrder": "Sıralı Liste", + "blockquote": "Alıntı", + "heading1": "Başlık 1", + "heading2": "Başlık 2", + "heading3": "Başlık 3", + "heading4": "Başlık 4", + "heading5": "Başlık 5", + "heading6": "Başlık 6", + "hr": "Yatay Çizgi" + }, + "editor": { + "words": {}, + "image": { + "selector": { + "tab": { + "select": "Seç", + "embedLink": "Bağlantı göm" + }, + "select": { + "chooseButton": "Görsel Seç", + "tip": "Bilgisayarınızdan bir görsel seçin." + }, + "inputs": { + "alt": "Alternatif metin", + "src": "Görsel bağlantısı veya yerel yol", + "title": "Görsel başlığı" + }, + "embedButton": "Görseli Göm", + "hint": { + "prefix": "Web görseli veya yerel görsel yolu yapıştırın. Şunu kullanın:", + "simple": "basit mod", + "full": "tam mod" + } + }, + "toolbar": { + "edit": "Görseli Düzenle", + "inline": "Satır İçi Görsel", + "alignLeft": "Sola Hizala", + "alignCenter": "Ortala", + "alignRight": "Sağa Hizala", + "delete": "Görseli Kaldır" + } + }, + "placeholders": { + "frontMatter": "YAML Ön Bilgisini girin", + "languageIdentifier": "Dil Tanımlayıcısını girin...", + "mathFormula": "Matematiksel Formülü girin..." + }, + "export": { + "error": "Dışa aktarma hatası", + "errorExporting": "{type} dosyası dışa aktarılırken hata", + "failed": "{type} dışa aktarma başarısız" + }, + "insertTable": { + "title": "Tablo Ekle", + "rows": "Satırlar", + "columns": "Sütunlar" + }, + "notifications": { + "notificationNotFound": "Bildirim bulunamadı" + }, + "print": { + "error": "\"{title}\" için yazdırma hatası", + "failed": "Yazdırma başarısız" + }, + "sourceCode": { + "cursorNullComment": "İmleç boş yorumu", + "imageStructureDeletedComment": "Görsel yapısı silindi yorumu" + }, + "spellcheck": { + "disabledError": "Yazım denetimi devre dışı hatası", + "errorSwitchingLanguage": "Yazım denetimi dili değiştirilirken hata {languageCode}", + "languageMissing": "Yazım denetimi dili eksik {languageCode}", + "switchError": "{languageCode} için yazım denetimi değiştirme hatası, {error}", + "title": "Yazım Denetimi", + "disabled": "Yazım denetimi devre dışı", + "enabled": "Yazım denetimi etkin", + "enabledError": "Yazım denetimi etkinleştirilirken hata" + }, + "table": "tablo", + "resizeTable": "Tabloyu Yeniden Boyutlandır", + "left": "sol", + "alignLeft": "Sola Hizala", + "center": "orta", + "alignCenter": "Ortala", + "right": "sağ", + "alignRight": "Sağa Hizala", + "delete": "sil", + "deleteTable": "Tabloyu Sil", + "insertRowAbove": "Üste Satır Ekle", + "insertRowBelow": "Alta Satır Ekle", + "removeRow": "Satırı Kaldır", + "insertColumnLeft": "Sola Sütun Ekle", + "insertColumnRight": "Sağa Sütun Ekle", + "removeColumn": "Sütunu Kaldır", + "copyContent": "İçeriği kopyala", + "emptyMathFormula": "< Boş Matematiksel Formül >", + "invalidMathFormula": "< Geçersiz Matematiksel Formül >", + "emptyMermaidBlock": "< Boş Mermaid Bloğu >", + "loading": "Yükleniyor...", + "emptyDiagramBlock": "< Boş Diyagram Bloğu >", + "emptyHtmlBlock": "< Boş HTML Bloğu >", + "onlyTopBlockCanRenderIcon": "Yalnızca en üstteki blok ön simge düğmesini işleyebilir.", + "unhandledFunctionType": "İşlenmeyen functionType: {functionType}", + "highlight-start": "[highlight end]", + "highlight-end": "[highlight end]", + "type-at-to-insert": "Eklemek için / yazın", + "input-footnote-definition": "Dipnot tanımını girin", + "input-yaml-front-matter": "YAML ön bilgisini girin", + "input-language-identifier": "Dil tanımlayıcısını girin", + "input-mathematical-formula": "Matematiksel formülü girin", + "fence": "kod bloğu", + "indent": "girinti", + "front-matter-delimiter": "ön bilgi ayracı", + "math-delimiter": "matematik ayracı", + "mermaid-start": "mermaid başlangıcı", + "flowchart-start": "akış şeması başlangıcı", + "sequence-start": "dizi başlangıcı", + "plantuml-start": "plantuml başlangıcı", + "vega-lite-start": "vega-lite başlangıcı", + "click-to-add-image": "Görsel eklemek için tıklayın", + "load-image-failed": "Görsel yüklenemedi" + } +} diff --git a/packages/desktop/static/locales/zh-CN.json b/packages/desktop/static/locales/zh-CN.json index 784e99fe64..ca7edcf9e3 100644 --- a/packages/desktop/static/locales/zh-CN.json +++ b/packages/desktop/static/locales/zh-CN.json @@ -455,6 +455,7 @@ "isHtmlEnabled": "启用 HTML 渲染", "isGitlabCompatibilityEnabled": "启用 GitLab 兼容模式", "sequenceTheme": "序列图主题", + "plantumlServer": "PlantUML 服务器 URL", "theme": "选择 MarkText 中使用的主题", "followSystemTheme": "跟随系统主题", "lightModeTheme": "系统处于浅色模式时使用的主题", @@ -552,7 +553,8 @@ "german": "Deutsch", "japanese": "日本語", "korean": "한국어", - "portuguese": "Português" + "portuguese": "Português", + "turkish": "Türkçe" } } }, @@ -771,6 +773,9 @@ "title": "序列图主题", "handDrawn": "手绘", "simple": "简单" + }, + "plantumlServer": { + "title": "PlantUML 服务器 URL" } }, "math": { diff --git a/packages/desktop/static/locales/zh-TW.json b/packages/desktop/static/locales/zh-TW.json index a9bff1cf0e..fba65bfa3d 100644 --- a/packages/desktop/static/locales/zh-TW.json +++ b/packages/desktop/static/locales/zh-TW.json @@ -455,6 +455,7 @@ "isHtmlEnabled": "啟用 HTML 轉譯", "isGitlabCompatibilityEnabled": "啟用 GitLab 相容模式", "sequenceTheme": "序列圖主題", + "plantumlServer": "PlantUML 伺服器 URL", "theme": "選擇 MarkText 中使用的主題", "followSystemTheme": "跟隨系統主題", "lightModeTheme": "系統處於淺色模式時使用的主題", @@ -552,7 +553,8 @@ "german": "Deutsch", "japanese": "日本語", "korean": "한국어", - "portuguese": "Português" + "portuguese": "Português", + "turkish": "Türkçe" } } }, @@ -772,6 +774,9 @@ "title": "序列主題", "handDrawn": "手繪", "simple": "簡單" + }, + "plantumlServer": { + "title": "PlantUML 伺服器 URL" } }, "math": { diff --git a/packages/desktop/static/preference.json b/packages/desktop/static/preference.json index d5aaca3c9c..c3d757e608 100644 --- a/packages/desktop/static/preference.json +++ b/packages/desktop/static/preference.json @@ -55,6 +55,7 @@ "isHtmlEnabled": true, "isGitlabCompatibilityEnabled": false, "sequenceTheme": "hand", + "plantumlServer": "https://www.plantuml.com/plantuml", "theme": "light", "followSystemTheme": true, diff --git a/packages/desktop/test/e2e/plantuml.spec.ts b/packages/desktop/test/e2e/plantuml.spec.ts index 8cabc85fbc..10c24d2eb4 100644 --- a/packages/desktop/test/e2e/plantuml.spec.ts +++ b/packages/desktop/test/e2e/plantuml.spec.ts @@ -8,6 +8,7 @@ import { launchWithMarkdown, focusEditor } from './helpers' // prefix, unlike the legacy pako path). const PLANTUML_DOC = '# plantuml smoke\n\n```plantuml\n@startuml\nA -> B\n@enduml\n```\n' +const CUSTOM_SERVER = 'http://localhost:9999/plantuml' test.describe('PlantUML render via plantuml-encoder', () => { let app: ElectronApplication @@ -24,7 +25,7 @@ test.describe('PlantUML render via plantuml-encoder', () => { if (app) await app.close() }) - test('plantuml block renders an img with a plantuml.com src', async() => { + test('plantuml block renders an img with the default plantuml.com src', async() => { // Muya renders code-block diagrams lazily; wait for the img to appear. const img = page.locator('img[src*="plantuml.com/plantuml"]').first() await expect(img).toHaveCount(1, { timeout: 10000 }) @@ -33,4 +34,24 @@ test.describe('PlantUML render via plantuml-encoder', () => { // `~1` deflate prefix (the legacy pako path used `~1`). expect(src).toMatch(/^https:\/\/www\.plantuml\.com\/plantuml\/svg\/[A-Za-z0-9_-]+$/) }) + + test('plantuml block uses custom server URL when preference is set', async() => { + // Set a custom PlantUML server URL via the preference system. + await page.evaluate((url) => { + window.electron.ipcRenderer.send('mt::set-user-preference', { plantumlServer: url }) + }, CUSTOM_SERVER) + + // Re-focus the editor to trigger a re-render with the new option. + await focusEditor(page) + + // Wait for the new img element pointing at the custom server. + const img = page.locator(`img[src*="${CUSTOM_SERVER}"]`).first() + await expect(img).toHaveCount(1, { timeout: 10000 }) + const src = await img.getAttribute('src') + expect(src).toMatch(new RegExp(`^${escapeRegex(CUSTOM_SERVER)}/svg/[A-Za-z0-9_-]+$`)) + }) }) + +function escapeRegex(str: string): string { + return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +} diff --git a/packages/muya/e2e/tests/editing/selection.spec.ts b/packages/muya/e2e/tests/editing/selection.spec.ts index 05f82ba1c4..d17ea193e2 100644 --- a/packages/muya/e2e/tests/editing/selection.spec.ts +++ b/packages/muya/e2e/tests/editing/selection.spec.ts @@ -17,8 +17,8 @@ test.describe('selection', () => { const sel = window.muya!.editor.selection.getSelection(); if (!sel) return false; - const { anchorBlock, focusBlock } = sel; - return anchorBlock !== focusBlock; + const { anchor, focus } = sel; + return anchor.block !== focus.block; }); expect(hasMultiBlockSelection).toBe(true); }); diff --git a/packages/muya/src/__tests__/createTableImageCursor.spec.ts b/packages/muya/src/__tests__/createTableImageCursor.spec.ts index 747fd86457..23485784e8 100644 --- a/packages/muya/src/__tests__/createTableImageCursor.spec.ts +++ b/packages/muya/src/__tests__/createTableImageCursor.spec.ts @@ -101,7 +101,7 @@ describe('muya.createTable()', () => { const sel = muya.editor.selection.getSelection(); expect(sel).not.toBeNull(); // The caret lands on a table-cell content block. - expect(sel!.anchorBlock.blockName).toBe('table.cell.content'); + expect(sel!.anchor.block.blockName).toBe('table.cell.content'); }); it('is a no-op when there is no current block', () => { @@ -242,7 +242,7 @@ describe('muya.setCursor()', () => { await vi.waitFor(() => { const sel = muya.editor.selection.getSelection(); expect(sel).not.toBeNull(); - expect(sel!.anchorBlock).toBe(first); + expect(sel!.anchor.block).toBe(first); expect(sel!.anchor.offset).toBe(3); }); }); @@ -257,7 +257,7 @@ describe('muya.setCursor()', () => { }); await vi.waitFor(() => { const sel = muya.editor.selection.getSelection(); - expect(sel!.anchorBlock).toBe(first); + expect(sel!.anchor.block).toBe(first); expect(sel!.anchor.offset).toBe(2); }); }); @@ -275,7 +275,7 @@ describe('muya.setCursor()', () => { }); await vi.waitFor(() => { const sel = muya.editor.selection.getSelection(); - expect(sel!.anchorBlock).toBe(secondContent); + expect(sel!.anchor.block).toBe(secondContent); expect(sel!.anchor.offset).toBe(1); }); }); diff --git a/packages/muya/src/__tests__/getCursorOffset.spec.ts b/packages/muya/src/__tests__/getCursorOffset.spec.ts index 2581acd4df..c51f4949ab 100644 --- a/packages/muya/src/__tests__/getCursorOffset.spec.ts +++ b/packages/muya/src/__tests__/getCursorOffset.spec.ts @@ -87,12 +87,8 @@ describe('muya.getCursorOffset() (Phase G — G7)', () => { const muya = bootMuya('hello world\n'); const block = muya.editor.scrollPage!.firstContentInDescendant()!; const selection: ISelection = { - anchor: { offset: 0 }, - focus: { offset: 5 }, - anchorBlock: block, - anchorPath: [0, 'text'], - focusBlock: block, - focusPath: [0, 'text'], + anchor: { offset: 0, block, path: [0, 'text'] }, + focus: { offset: 5, block, path: [0, 'text'] }, isCollapsed: false, isSelectionInSameBlock: true, direction: 'forward', @@ -111,12 +107,8 @@ describe('muya.getCursorOffset() (Phase G — G7)', () => { const muya = bootMuya('hello world\n'); const block = muya.editor.scrollPage!.firstContentInDescendant()!; const selection: ISelection = { - anchor: { offset: 9 }, // after "hello wor" - focus: { offset: 2 }, // after "he" - anchorBlock: block, - anchorPath: [0, 'text'], - focusBlock: block, - focusPath: [0, 'text'], + anchor: { offset: 9, block, path: [0, 'text'] }, // after "hello wor" + focus: { offset: 2, block, path: [0, 'text'] }, // after "he" isCollapsed: false, isSelectionInSameBlock: true, direction: 'backward', diff --git a/packages/muya/src/__tests__/historySerialization.spec.ts b/packages/muya/src/__tests__/historySerialization.spec.ts index 255c5f9041..969f8af2b1 100644 --- a/packages/muya/src/__tests__/historySerialization.spec.ts +++ b/packages/muya/src/__tests__/historySerialization.spec.ts @@ -93,7 +93,7 @@ describe('muya history serialization api', () => { if (recorded.selection) { expect(recorded.selection).not.toHaveProperty('anchorBlock'); expect(recorded.selection).not.toHaveProperty('focusBlock'); - expect(Array.isArray(recorded.selection.anchorPath)).toBe(true); + expect(Array.isArray(recorded.selection.anchor.path)).toBe(true); } }); diff --git a/packages/muya/src/__tests__/localeRefresh.spec.ts b/packages/muya/src/__tests__/localeRefresh.spec.ts index f1be515898..7036e4bb85 100644 --- a/packages/muya/src/__tests__/localeRefresh.spec.ts +++ b/packages/muya/src/__tests__/localeRefresh.spec.ts @@ -108,7 +108,7 @@ describe('muya.locale() refreshes rendered hints (Phase G — G8)', () => { const sel = muya.editor.selection.getSelection(); expect(sel).not.toBeNull(); expect(sel!.anchor.offset).toBe(5); - expect(sel!.anchorBlock.text).toBe('hello world'); + expect(sel!.anchor.block.text).toBe('hello world'); }); it('is a no-op-safe re-render when the tree is not yet mounted', () => { diff --git a/packages/muya/src/__tests__/replaceContent.spec.ts b/packages/muya/src/__tests__/replaceContent.spec.ts index 19c3fabd8c..654f576cfa 100644 --- a/packages/muya/src/__tests__/replaceContent.spec.ts +++ b/packages/muya/src/__tests__/replaceContent.spec.ts @@ -214,7 +214,7 @@ describe('muya replaceContent — single undo boundary', () => { // @ts-expect-error — reach into the private stack for assertions. const storedSel = muya.editor.history._stack.undo[0].selection; - const pathLenBefore = storedSel?.anchorPath?.length ?? 0; + const pathLenBefore = storedSel?.anchor.path?.length ?? 0; expect(pathLenBefore).toBeGreaterThan(0); // Two full undo/redo cycles — each replay resolves the caret from paths. @@ -237,7 +237,7 @@ describe('muya replaceContent — single undo boundary', () => { // @ts-expect-error — private stack read. const after = muya.editor.history._stack; const finalSel = after.redo[0]?.selection ?? after.undo[0]?.selection; - expect(finalSel?.anchorPath?.length ?? 0).toBeGreaterThan(0); + expect(finalSel?.anchor.path?.length ?? 0).toBeGreaterThan(0); }); it('does not coalesce a later edit into the replacement boundary', async () => { @@ -356,7 +356,7 @@ describe('muya replaceContent — single undo boundary', () => { muya.editor.activeContentBlock = second; second.setCursor(1, 1, true); const recordSelection = muya.getSelection(); - const recordPath = recordSelection?.anchorPath; + const recordPath = recordSelection?.anchor.path; expect(recordPath?.length ?? 0).toBeGreaterThan(0); // Move the LIVE caret to the FIRST block — this is what an unguarded @@ -364,7 +364,7 @@ describe('muya replaceContent — single undo boundary', () => { const first = muya.editor.scrollPage!.firstContentInDescendant()!; muya.editor.activeContentBlock = first; first.setCursor(0, 0, true); - const livePath = muya.getSelection()?.anchorPath; + const livePath = muya.getSelection()?.anchor.path; expect(livePath).not.toEqual(recordPath); muya.replaceContent('first\n\nsecond\n\nthird\n', recordSelection); @@ -373,7 +373,7 @@ describe('muya replaceContent — single undo boundary', () => { // not the live DOM caret (first block). // @ts-expect-error — reach into the private stack for assertions. const storedSel = muya.editor.history._stack.undo[0].selection; - expect(storedSel?.anchorPath).toEqual(recordPath); + expect(storedSel?.anchor.path).toEqual(recordPath); }); it('accepts a state array as well as markdown', async () => { diff --git a/packages/muya/src/__tests__/setCursorByOffset.spec.ts b/packages/muya/src/__tests__/setCursorByOffset.spec.ts index e2dda418d3..d7dabd6f29 100644 --- a/packages/muya/src/__tests__/setCursorByOffset.spec.ts +++ b/packages/muya/src/__tests__/setCursorByOffset.spec.ts @@ -53,7 +53,7 @@ describe('muya.setCursorByOffset() (PG2)', () => { const sel = muya.editor.selection.getSelection(); expect(sel).not.toBeNull(); // The caret lands inside the "third para here" block. - expect(sel!.anchorBlock.text).toBe('third para here'); + expect(sel!.anchor.block.text).toBe('third para here'); expect(sel!.anchor.offset).toBe(6); }); // The document content is left clean (no sentinel residue). @@ -72,7 +72,7 @@ describe('muya.setCursorByOffset() (PG2)', () => { const sel = muya.editor.selection.getSelection(); // This engine keeps the `# ` marker in the heading content block's // text, so the caret lands at offset 4 of "# Title". - expect(sel!.anchorBlock.text).toBe('# Title'); + expect(sel!.anchor.block.text).toBe('# Title'); expect(sel!.anchor.offset).toBe(4); }); }); diff --git a/packages/muya/src/__tests__/updateParagraph.spec.ts b/packages/muya/src/__tests__/updateParagraph.spec.ts index 984ddf6e31..e71091cf5b 100644 --- a/packages/muya/src/__tests__/updateParagraph.spec.ts +++ b/packages/muya/src/__tests__/updateParagraph.spec.ts @@ -169,12 +169,8 @@ describe('muya.updateParagraph()', () => { // range, so the real menu scenario (getSelection returns the range) is // stubbed here; the assertions below exercise the path re-resolution. const liveSelection = { - anchor: { offset: 0 }, - focus: { offset: 1 }, - anchorBlock: first, - anchorPath: first.path, - focusBlock: third, - focusPath: third.path, + anchor: { offset: 0, block: first, path: first.path }, + focus: { offset: 1, block: third, path: third.path }, isCollapsed: false, isSelectionInSameBlock: false, direction: 'forward', diff --git a/packages/muya/src/block/base/content.ts b/packages/muya/src/block/base/content.ts index ff8adfae41..4a652c0dd2 100644 --- a/packages/muya/src/block/base/content.ts +++ b/packages/muya/src/block/base/content.ts @@ -296,15 +296,13 @@ class Content extends TreeNode { const { anchor, focus, - anchorBlock, - focusBlock, isCollapsed, isSelectionInSameBlock, // This is always be true. direction, type, } = selection; - if (anchorBlock !== this || focusBlock !== this) + if (anchor.block !== this || focus.block !== this) return null; return { diff --git a/packages/muya/src/block/extra/diagram/diagramPreview.ts b/packages/muya/src/block/extra/diagram/diagramPreview.ts index 12d6c747e1..d948621fc9 100644 --- a/packages/muya/src/block/extra/diagram/diagramPreview.ts +++ b/packages/muya/src/block/extra/diagram/diagramPreview.ts @@ -15,6 +15,7 @@ interface IRenderOptions { target: HTMLElement; vegaTheme: string; mermaidTheme: string; + plantumlServer: string; sequenceTheme: 'hand' | 'simple'; } @@ -24,6 +25,7 @@ async function renderDiagram({ target, vegaTheme, mermaidTheme, + plantumlServer, sequenceTheme, }: IRenderOptions) { const render = await loadRenderer(type); @@ -42,7 +44,7 @@ async function renderDiagram({ } if (type === 'plantuml') { - const diagram = render.parse(code); + const diagram = render.parse(code, plantumlServer); target.innerHTML = ''; diagram.insertImgElement(target); } @@ -128,7 +130,7 @@ class DiagramPreview extends Parent { if (code) { this.domNode!.innerHTML = i18n.t('Loading...'); - const { mermaidTheme, vegaTheme, sequenceTheme } = this.muya.options; + const { mermaidTheme, vegaTheme, plantumlServer, sequenceTheme } = this.muya.options; const { type } = this; try { @@ -138,6 +140,7 @@ class DiagramPreview extends Parent { type, mermaidTheme, vegaTheme, + plantumlServer, sequenceTheme, }); } diff --git a/packages/muya/src/clipboard/__tests__/clipboardFilePath.spec.ts b/packages/muya/src/clipboard/__tests__/clipboardFilePath.spec.ts index 0bf9062ac9..33e6681094 100644 --- a/packages/muya/src/clipboard/__tests__/clipboardFilePath.spec.ts +++ b/packages/muya/src/clipboard/__tests__/clipboardFilePath.spec.ts @@ -64,7 +64,7 @@ function makeClipboard( get: () => ({ getSelection: () => ({ isSelectionInSameBlock: true, - anchorBlock, + anchor: { block: anchorBlock }, }), }), }); diff --git a/packages/muya/src/clipboard/__tests__/getClipboardData.spec.ts b/packages/muya/src/clipboard/__tests__/getClipboardData.spec.ts index 2871d8d97d..e03fbb2b8b 100644 --- a/packages/muya/src/clipboard/__tests__/getClipboardData.spec.ts +++ b/packages/muya/src/clipboard/__tests__/getClipboardData.spec.ts @@ -36,10 +36,8 @@ function selectionOver( const block = { text, blockName } as unknown as Content; return { isSelectionInSameBlock: true, - anchor: { offset: begin }, - focus: { offset: end }, - anchorBlock: block, - focusBlock: block, + anchor: { offset: begin, block, path: [] }, + focus: { offset: end, block, path: [] }, }; } diff --git a/packages/muya/src/clipboard/__tests__/parityImagePaste.spec.ts b/packages/muya/src/clipboard/__tests__/parityImagePaste.spec.ts index e67f0f614a..41ea326cc2 100644 --- a/packages/muya/src/clipboard/__tests__/parityImagePaste.spec.ts +++ b/packages/muya/src/clipboard/__tests__/parityImagePaste.spec.ts @@ -68,7 +68,7 @@ function makeClipboard(options: Record, anchorBlock: Content) { get: () => ({ getSelection: () => ({ isSelectionInSameBlock: true, - anchorBlock, + anchor: { block: anchorBlock }, }), }), }); diff --git a/packages/muya/src/clipboard/__tests__/pasteHandlerParity.spec.ts b/packages/muya/src/clipboard/__tests__/pasteHandlerParity.spec.ts index ba5b19f9a5..911fd0a07e 100644 --- a/packages/muya/src/clipboard/__tests__/pasteHandlerParity.spec.ts +++ b/packages/muya/src/clipboard/__tests__/pasteHandlerParity.spec.ts @@ -124,7 +124,7 @@ function makeClipboard( } as unknown as Muya); Object.defineProperty(clipboard, 'selection', { get: () => ({ - getSelection: () => ({ isSelectionInSameBlock: true, anchorBlock }), + getSelection: () => ({ isSelectionInSameBlock: true, anchor: { block: anchorBlock } }), table: tableStub, }), }); diff --git a/packages/muya/src/clipboard/__tests__/pasteImageLoading.spec.ts b/packages/muya/src/clipboard/__tests__/pasteImageLoading.spec.ts index 10a3492301..326e6924e8 100644 --- a/packages/muya/src/clipboard/__tests__/pasteImageLoading.spec.ts +++ b/packages/muya/src/clipboard/__tests__/pasteImageLoading.spec.ts @@ -48,7 +48,7 @@ function makeClipboard(options: Record, anchorBlock: Content) { get: () => ({ getSelection: () => ({ isSelectionInSameBlock: true, - anchorBlock, + anchor: { block: anchorBlock }, }), }), }); diff --git a/packages/muya/src/clipboard/__tests__/trackCCut.spec.ts b/packages/muya/src/clipboard/__tests__/trackCCut.spec.ts index 5bc4d2e396..248d1d9b64 100644 --- a/packages/muya/src/clipboard/__tests__/trackCCut.spec.ts +++ b/packages/muya/src/clipboard/__tests__/trackCCut.spec.ts @@ -81,12 +81,8 @@ function stubSelection( const aPath = a.path; const fPath = f.path; muya.editor.selection.getSelection = () => ({ - anchor: { offset: aOff }, - focus: { offset: fOff }, - anchorBlock: a, - anchorPath: aPath, - focusBlock: f, - focusPath: fPath, + anchor: { offset: aOff, block: a, path: aPath }, + focus: { offset: fOff, block: f, path: fPath }, isCollapsed: false, isSelectionInSameBlock: a === f, direction, diff --git a/packages/muya/src/clipboard/copyData.ts b/packages/muya/src/clipboard/copyData.ts index 22b31d32aa..54b609c30e 100644 --- a/packages/muya/src/clipboard/copyData.ts +++ b/packages/muya/src/clipboard/copyData.ts @@ -107,7 +107,9 @@ function resolveSelectionOrder( clipboard: Clipboard, selection: ISelection, ): Nullable { - const { anchor, anchorBlock, focus, focusBlock } = selection; + const { anchor, focus } = selection; + const anchorBlock = anchor.block; + const focusBlock = focus.block; const anchorOutMostBlock = anchorBlock.outMostBlock!; const focusOutMostBlock = focusBlock.outMostBlock!; const anchorOutMostBlockOffset = clipboard.scrollPage?.offset(anchorOutMostBlock); @@ -257,8 +259,9 @@ export function getClipboardData(clipboard: Clipboard): IClipboardPayload { if (selection == null) return { html: '', text: '' }; - const { isSelectionInSameBlock, anchor, anchorBlock, focus, focusBlock } - = selection; + const { isSelectionInSameBlock, anchor, focus } = selection; + const anchorBlock = anchor.block; + const focusBlock = focus.block; if (anchorBlock == null || focusBlock == null) return { html: '', text: '' }; diff --git a/packages/muya/src/clipboard/cut.ts b/packages/muya/src/clipboard/cut.ts index 4bbdc1bf8d..a8e2710e19 100644 --- a/packages/muya/src/clipboard/cut.ts +++ b/packages/muya/src/clipboard/cut.ts @@ -322,11 +322,11 @@ export function cutSelection(clipboard: Clipboard): void { const { isSelectionInSameBlock, anchor, - anchorBlock, focus, - focusBlock, direction, } = selection; + const anchorBlock = anchor.block; + const focusBlock = focus.block; // Handler `cut` event in the same block. if (isSelectionInSameBlock) { diff --git a/packages/muya/src/clipboard/paste.ts b/packages/muya/src/clipboard/paste.ts index 66fb99ba7e..6861a7e757 100644 --- a/packages/muya/src/clipboard/paste.ts +++ b/packages/muya/src/clipboard/paste.ts @@ -219,7 +219,8 @@ async function applyPaste(clipboard: Clipboard, data: IPasteData): Promise if (!selection) return; - const { isSelectionInSameBlock, anchorBlock } = selection; + const { isSelectionInSameBlock, anchor } = selection; + const anchorBlock = anchor.block; if (!anchorBlock) return; diff --git a/packages/muya/src/config/index.ts b/packages/muya/src/config/index.ts index 0b459a48e4..65c6cd65f4 100644 --- a/packages/muya/src/config/index.ts +++ b/packages/muya/src/config/index.ts @@ -323,6 +323,7 @@ export const MUYA_DEFAULT_OPTIONS = { frontmatterType: '-', mermaidTheme: 'default', // dark / forest / default vegaTheme: 'latimes', // excel / ggplot2 / quartz / vox / fivethirtyeight / dark / latimes + plantumlServer: 'https://www.plantuml.com/plantuml', sequenceTheme: 'hand' as 'hand' | 'simple', // hand / simple hideQuickInsertHint: false, hideLinkPopup: false, diff --git a/packages/muya/src/editor/index.ts b/packages/muya/src/editor/index.ts index abca5a9e60..e688bcecc4 100644 --- a/packages/muya/src/editor/index.ts +++ b/packages/muya/src/editor/index.ts @@ -86,8 +86,9 @@ export class Editor { const { domNode } = this.muya; const eventHandler = (event: Event) => { - const { anchorBlock, isSelectionInSameBlock } - = this.selection.getSelection() ?? {}; + const selectionResult = this.selection.getSelection(); + const anchorBlock = selectionResult?.anchor.block; + const isSelectionInSameBlock = selectionResult?.isSelectionInSameBlock; // Fix issue that language input can not get focus when it's empty(Firefox only) if ( event.type === 'click' @@ -385,10 +386,10 @@ export class Editor { if (!selection) return; - const { anchorPath, anchor, focus, isSelectionInSameBlock } = selection; + const { anchor, focus, isSelectionInSameBlock } = selection; // `ScrollPage.queryBlock` consumes the path array in place (`path.shift`), // so query against a copy and leave the caller's selection untouched. - const cursorBlock = this.scrollPage?.queryBlock([...anchorPath]); + const cursorBlock = this.scrollPage?.queryBlock([...anchor.path]); const begin = Math.min(anchor.offset, focus.offset); const end = Math.max(anchor.offset, focus.offset); @@ -418,9 +419,16 @@ export class Editor { // paths so `_setCursor`'s `queryBlock(path)` fallback can't drain the // caller's arrays — notably the selection object stored in the undo stack. this.selection.setSelection({ - ...selection, - anchorPath: [...selection.anchorPath], - focusPath: [...selection.focusPath], + anchor, + focus, + anchorBlock: anchor.block, + anchorPath: [...anchor.path], + focusBlock: focus.block, + focusPath: [...focus.path], + isCollapsed: selection.isCollapsed, + isSelectionInSameBlock, + direction: selection.direction, + type: selection.type, }); } diff --git a/packages/muya/src/history/index.ts b/packages/muya/src/history/index.ts index d245c130d4..61d7865179 100644 --- a/packages/muya/src/history/index.ts +++ b/packages/muya/src/history/index.ts @@ -1,6 +1,6 @@ import type { JSONOpList } from 'ot-json1'; import type { Muya } from '../muya'; -import type { IHistorySelection } from '../selection/types'; +import type { IAnchorFocusInfo, IHistorySelection } from '../selection/types'; import type { TState } from '../state/types'; import type { Nullable } from '../types'; import * as json1 from 'ot-json1'; @@ -33,16 +33,16 @@ interface IStack { redo: IOperation[]; } -// A JSON-serializable view of an ISelection. The live `anchorBlock` / -// `focusBlock` references are dropped — they are an in-memory optimization -// only. `Selection._setCursor` re-resolves the target block from -// `anchorPath` / `focusPath` via `scrollPage.queryBlock(path)` when no block -// instance is present, so a path-only selection restores the caret losslessly. +// A JSON-serializable view of an ISelection. The live endpoint `block` +// references are dropped — they are an in-memory optimization only. +// `Selection._setCursor` re-resolves the target block from each endpoint's +// `path` via `scrollPage.queryBlock(path)` when no block instance is present, +// so a path-only selection restores the caret losslessly. +type ISerializableAnchorFocusInfo = Pick; + interface ISerializableSelection { - anchor: IHistorySelection['anchor']; - focus: IHistorySelection['focus']; - anchorPath: IHistorySelection['anchorPath']; - focusPath: IHistorySelection['focusPath']; + anchor: ISerializableAnchorFocusInfo; + focus: ISerializableAnchorFocusInfo; isCollapsed: IHistorySelection['isCollapsed']; isSelectionInSameBlock: IHistorySelection['isSelectionInSameBlock']; direction: IHistorySelection['direction']; @@ -215,10 +215,8 @@ class History { return selection; return { - anchor: deepClone(selection.anchor), - focus: deepClone(selection.focus), - anchorPath: deepClone(selection.anchorPath), - focusPath: deepClone(selection.focusPath), + anchor: { offset: selection.anchor.offset, path: deepClone(selection.anchor.path) }, + focus: { offset: selection.focus.offset, path: deepClone(selection.focus.path) }, isCollapsed: selection.isCollapsed, isSelectionInSameBlock: selection.isSelectionInSameBlock, direction: selection.direction, @@ -229,9 +227,9 @@ class History { // Rebuild a selection without live block references. The block instances // are intentionally omitted: the only consumers of a restored selection // are `editor.updateContents` and `selection._setCursor`, both of which - // re-resolve the target block from `anchorPath` / `focusPath` via + // re-resolve the target block from each endpoint's `path` via // `scrollPage.queryBlock` when no block instance is present. The return - // type is `IHistorySelection`, whose `anchorBlock` / `focusBlock` are + // type is `IHistorySelection`, whose endpoint `block` references are // optional, so the missing block fields are part of the contract rather // than an unsound cast over fabricated `ContentBlock` instances. private _fromSerializableSelection( @@ -241,10 +239,8 @@ class History { return selection; return { - anchor: deepClone(selection.anchor), - focus: deepClone(selection.focus), - anchorPath: deepClone(selection.anchorPath), - focusPath: deepClone(selection.focusPath), + anchor: { offset: selection.anchor.offset, path: deepClone(selection.anchor.path) }, + focus: { offset: selection.focus.offset, path: deepClone(selection.focus.path) }, isCollapsed: selection.isCollapsed, isSelectionInSameBlock: selection.isSelectionInSameBlock, direction: selection.direction, diff --git a/packages/muya/src/index.ts b/packages/muya/src/index.ts index 0c6ac2ad92..b3a23f155a 100644 --- a/packages/muya/src/index.ts +++ b/packages/muya/src/index.ts @@ -1,5 +1,5 @@ export type { ILocale } from './i18n/types'; -export { de, en, es, fr, ja, ko, pt, zhCN, zhTW } from './locales'; +export { de, en, es, fr, ja, ko, pt, tr, zhCN, zhTW } from './locales'; export { Muya } from './muya'; export type { ITocItem } from './state/getTOC'; diff --git a/packages/muya/src/locales/index.ts b/packages/muya/src/locales/index.ts index d4ffd178ea..5f51cc59af 100644 --- a/packages/muya/src/locales/index.ts +++ b/packages/muya/src/locales/index.ts @@ -5,5 +5,6 @@ export { fr } from './fr'; export { ja } from './ja'; export { ko } from './ko'; export { pt } from './pt'; +export { tr } from './tr'; export { zhCN } from './zh-CN'; export { zhTW } from './zh-TW'; diff --git a/packages/muya/src/locales/tr.ts b/packages/muya/src/locales/tr.ts new file mode 100644 index 0000000000..2fe023fd82 --- /dev/null +++ b/packages/muya/src/locales/tr.ts @@ -0,0 +1,102 @@ +export const tr = { + name: 'tr', + resource: { + // tableTools + 'Insert Row Above': 'Üste Satır Ekle', + 'Insert Row Below': 'Alta Satır Ekle', + 'Remove Row': 'Satırı Kaldır', + // tableColumnTools + 'Align Left': 'Sola Hizala', + 'Align Center': 'Ortala', + 'Align Right': 'Sağa Hizala', + 'Insert Column left': 'Sola Sütun Ekle', + 'Insert Column right': 'Sağa Sütun Ekle', + 'Remove Column': 'Sütunu Kaldır', + // quickInsert + 'Paragraph': 'Paragraf', + 'Horizontal Line': 'Yatay Çizgi', + 'Front Matter': 'Ön Bilgi', + 'Header 1': 'Başlık 1', + 'Header 2': 'Başlık 2', + 'Header 3': 'Başlık 3', + 'Header 4': 'Başlık 4', + 'Header 5': 'Başlık 5', + 'Header 6': 'Başlık 6', + 'Table Block': 'Tablo Bloğu', + 'Display Math': 'Matematik Bloğu', + 'HTML Block': 'HTML Bloğu', + 'Code Block': 'Kod Bloğu', + 'Quote Block': 'Alıntı Bloğu', + 'Order List': 'Sıralı Liste', + 'Bullet List': 'Sırasız Liste', + 'To-do List': 'Yapılacaklar Listesi', + 'Vega Chart': 'Vega Grafiği', + 'Mermaid': 'Mermaid', + 'Plantuml': 'Plantuml', + 'basic blocks': 'temel bloklar', + 'headers': 'başlıklar', + 'advanced blocks': 'gelişmiş bloklar', + 'list blocks': 'liste blokları', + 'diagrams': 'diyagramlar', + 'No result': 'Sonuç yok', + 'Search keyword...': 'Anahtar sözcük ara...', + 'Type / to insert...': 'Eklemek için / yazın...', + 'Copy anchor link to this heading': 'Bu başlığın çapa bağlantısını kopyala', + 'Click to add an image': 'Görsel eklemek için tıklayın', + 'Load image failed': 'Görsel yüklenemedi', + // formatPicker + 'Emphasize': 'Kalın', + 'Italic': 'İtalik', + 'Underline': 'Altı Çizili', + 'Strikethrough': 'Üstü Çizili', + 'Highlight': 'Vurgu', + 'Inline Code': 'Satır İçi Kod', + 'Inline Math': 'Satır İçi Matematik', + 'Link': 'Bağlantı', + 'Image': 'Görsel', + 'Eliminate': 'Temizle', + // Code block + 'Copy content': 'İçeriği kopyala', + 'Input Language Identifier...': 'Dil Tanımlayıcısını girin...', + // emojiPicker + 'Smileys & Emotion': 'Suratlar ve Duygular', + 'People & Body': 'İnsanlar ve Beden', + 'Animals & Nature': 'Hayvanlar ve Doğa', + 'Food & Drink': 'Yiyecek ve İçecek', + 'Travel & Places': 'Seyahat ve Yerler', + 'Activities': 'Etkinlikler', + 'Objects': 'Nesneler', + 'Symbols': 'Semboller', + 'Flags': 'Bayraklar', + // frontMenu + 'Duplicate': 'Çoğalt', + 'New Paragraph': 'Yeni Paragraf', + 'Delete': 'Sil', + // imageToolbar + 'Edit Image': 'Görseli Düzenle', + 'Inline Image': 'Satır İçi Görsel', + 'Remove Image': 'Görseli Kaldır', + // ImageSelector + 'Image src placeholder': 'Görsel kaynağı', + 'Confirm Text': 'Tamam', + 'Select': 'Seç', + 'Embed link': 'Bağlantı göm', + 'Choose Image': 'Görsel Seç', + 'Choose image from your computer.': 'Bilgisayarınızdan bir görsel seçin.', + 'Alt text': 'Alternatif metin', + 'Image link or local path': 'Görsel bağlantısı veya yerel yol', + 'Image title': 'Görsel başlığı', + 'Embed Image': 'Görseli Göm', + 'Paste web image or local image path. Use': 'Web görseli veya yerel görsel yolu yapıştırın. Şunu kullanın:', + 'simple mode': 'basit mod', + 'full mode': 'tam mod', + // preview block + 'Loading...': 'Yükleniyor...', + 'Invalid Diagram Code': 'Geçersiz Diyagram Kodu', + 'Empty Diagram': 'Boş Diyagram', + 'Input Mathematical Formula...': 'Matematiksel Formülü girin...', + 'Input Front Matter...': 'Ön Bilgiyi girin...', + 'Invalid Mathematical Formula': 'Geçersiz Matematiksel Formül', + 'Empty Mathematical Formula': 'Boş Matematiksel Formül', + }, +}; diff --git a/packages/muya/src/muya.ts b/packages/muya/src/muya.ts index 4a117b6970..70cb4a9702 100644 --- a/packages/muya/src/muya.ts +++ b/packages/muya/src/muya.ts @@ -355,7 +355,7 @@ export class Muya { if (selection && selection.isSelectionInSameBlock) { const begin = Math.min(selection.anchor.offset, selection.focus.offset); const end = Math.max(selection.anchor.offset, selection.focus.offset); - const cursorBlock = this.editor.scrollPage?.queryBlock(selection.anchorPath); + const cursorBlock = this.editor.scrollPage?.queryBlock(selection.anchor.path); if (cursorBlock && cursorBlock.isContent()) cursorBlock.setCursor(begin, end, true); } @@ -414,15 +414,8 @@ export class Muya { if (!sel) return; - const { - anchor, - focus, - anchorBlock, - anchorPath, - focusBlock, - focusPath, - isSelectionInSameBlock, - } = sel; + const { anchor, focus, isSelectionInSameBlock } = sel; + const anchorBlock = anchor.block; if (!isSelectionInSameBlock || !(anchorBlock instanceof Format)) return; @@ -433,9 +426,9 @@ export class Muya { anchor, focus, anchorBlock, - anchorPath, - focusBlock, - focusPath, + anchorPath: anchor.path, + focusBlock: focus.block, + focusPath: focus.path, }); anchorBlock.format(type); @@ -1096,8 +1089,8 @@ export class Muya { const live = sel.getSelection(); const anchor = live?.anchor ?? sel.anchor; const focus = live?.focus ?? sel.focus; - const anchorPath = live?.anchorPath ?? sel.anchorPath; - const focusPath = live?.focusPath ?? sel.focusPath; + const anchorPath = live?.anchor.path ?? sel.anchorPath; + const focusPath = live?.focus.path ?? sel.focusPath; if (!anchor || !focus || !anchorPath?.length || !focusPath?.length) return null; diff --git a/packages/muya/src/selection/ImageSelection.ts b/packages/muya/src/selection/ImageSelection.ts index 59205c3697..8036f86dec 100644 --- a/packages/muya/src/selection/ImageSelection.ts +++ b/packages/muya/src/selection/ImageSelection.ts @@ -6,6 +6,7 @@ import { BLOCK_DOM_PROPERTY, CLASS_NAMES } from '../config'; import { isHTMLElement, isKeyboardEvent } from '../utils'; import { getImageInfo, getImageSrc } from '../utils/image'; import { findContentDOM } from './dom'; +import { SelectionType } from './types'; class ImageSelection { selected: IImageSelectionData | null = null; @@ -56,7 +57,7 @@ class ImageSelection { event.preventDefault(); const { block, ...imageInfo } = selected; block.deleteImage(imageInfo); - this._selection.activate('text'); + this._selection.activate(SelectionType.Text); } }; diff --git a/packages/muya/src/selection/TableRectSelection.ts b/packages/muya/src/selection/TableRectSelection.ts index 9b9c065cf1..a932bb7d23 100644 --- a/packages/muya/src/selection/TableRectSelection.ts +++ b/packages/muya/src/selection/TableRectSelection.ts @@ -261,7 +261,7 @@ class TableRectSelection { /** * The selected rectangle as an `ITableState` sub-table, or `null` when there - * is no frozen selection. The clipboard serialises this to GFM markdown. + * is no frozen selection. The clipboard serializes this to GFM markdown. */ getStateForCopy(): Nullable { if (!this.hasSelection) diff --git a/packages/muya/src/selection/TextSelection.ts b/packages/muya/src/selection/TextSelection.ts index d6448451ca..ef6b72dda2 100644 --- a/packages/muya/src/selection/TextSelection.ts +++ b/packages/muya/src/selection/TextSelection.ts @@ -1,6 +1,8 @@ import type Content from '../block/base/content'; import type Format from '../block/base/format'; +import type { TBlockPath } from '../block/types'; import type { Muya } from '../muya'; +import type { Nullable } from '../types'; import type Selection from './index'; import type { ICursor, INodeOffset, ISelection } from './types'; import { BLOCK_DOM_PROPERTY } from '../config'; @@ -16,15 +18,17 @@ import { getNodeAndOffset, getOffsetOfParagraph, } from './dom'; +import { SelectionType } from './types'; class TextSelection { - public doc: Document = document; - public anchorPath: (string | number)[] = []; - public anchorBlock: Content | null = null; - public focusPath: (string | number)[] = []; - public focusBlock: Content | null = null; - public anchor: INodeOffset | null = null; - public focus: INodeOffset | null = null; + public anchorPath: TBlockPath = []; + public anchorBlock: Nullable = null; + public focusPath: TBlockPath = []; + public focusBlock: Nullable = null; + public anchor: Nullable = null; + public focus: Nullable = null; + + private _doc: Document = document; private _selectInfo: { isSelect: boolean; @@ -45,16 +49,16 @@ class TextSelection { get isCollapsed() { const { anchorBlock, focusBlock, anchor, focus } = this; - if (anchor === null || focus === null) + if (anchor == null || focus == null) return false; return anchorBlock === focusBlock && anchor.offset === focus.offset; } get isSelectionInSameBlock() { - const { anchorBlock, focusBlock, anchor } = this; + const { anchorBlock, focusBlock, anchor, focus } = this; - if (anchor === null || focus === null) + if (anchor == null || focus == null) return false; return anchorBlock === focusBlock; @@ -69,7 +73,7 @@ class TextSelection { isSelectionInSameBlock, isCollapsed, } = this; - if (anchor === null || focus === null || !anchorBlock || !focusBlock) + if (anchor == null || focus == null || !anchorBlock || !focusBlock) return 'none'; if (isCollapsed) @@ -117,13 +121,13 @@ class TextSelection { }; this.setSelection(cursor); - const activeEle = this.doc.activeElement; + const activeEle = this._doc.activeElement; if (isHTMLElement(activeEle) && activeEle.classList.contains('mu-content')) activeEle.blur(); } getSelection(): ISelection | null { - const selection = document.getSelection(); + const selection = this._doc.getSelection(); if (!selection) return null; @@ -146,19 +150,18 @@ class TextSelection { // crashing — the caller treats null the same as "no selection". if (!anchorBlock || !focusBlock) return null; + const anchorPath = anchorBlock.path; const focusPath = focusBlock.path; - const aOffset - = getOffsetOfParagraph(anchorNode, anchorDomNode) + anchorOffset; + const aOffset = getOffsetOfParagraph(anchorNode, anchorDomNode) + anchorOffset; const fOffset = getOffsetOfParagraph(focusNode, focusDomNode) + focusOffset; const anchor = { offset: aOffset }; const focus = { offset: fOffset }; - const isCollapsed - = anchorBlock === focusBlock && anchor.offset === focus.offset; - + const isCollapsed = anchorBlock === focusBlock && anchor.offset === focus.offset; const isSelectionInSameBlock = anchorBlock === focusBlock; + let direction: string; if (isSelectionInSameBlock) { @@ -174,12 +177,8 @@ class TextSelection { const type = isCollapsed ? 'Caret' : 'Range'; return { - anchor, - focus, - anchorBlock, - anchorPath, - focusBlock, - focusPath, + anchor: { offset: anchor.offset, block: anchorBlock, path: anchorPath }, + focus: { offset: focus.offset, block: focusBlock, path: focusPath }, isCollapsed, isSelectionInSameBlock, direction, @@ -203,7 +202,7 @@ class TextSelection { this.anchorPath = anchorPath ?? path ?? []; this.focusBlock = focusBlock ?? block ?? null; this.focusPath = focusPath ?? path ?? []; - this._setCursor(); + this._updateSelection(); const { isCollapsed, @@ -243,8 +242,7 @@ class TextSelection { isSelectionInSameBlock, direction, type, - kind: 'text', - selection: this, + kind: SelectionType.Text, selectedImage: this._selection.image, cursorCoords, formats, @@ -289,18 +287,14 @@ class TextSelection { if (!selection) return; - const { - anchor, - focus, - anchorBlock, - focusBlock, - isSelectionInSameBlock, - } = selection; + const { anchor, focus, isSelectionInSameBlock } = selection; if (isSelectionInSameBlock) { return; } + const anchorBlock = anchor.block; + const focusBlock = focus.block; const newSelection = { anchor, focus, @@ -324,7 +318,7 @@ class TextSelection { } private _selectRange(range: Range) { - const selection = this.doc.getSelection(); + const selection = this._doc.getSelection(); if (selection) { selection.removeAllRanges(); @@ -338,7 +332,7 @@ class TextSelection { endNode?: Node, endOffset?: number, ) { - const range = this.doc.createRange(); + const range = this._doc.createRange(); range.setStart(startNode, startOffset); if (endNode && typeof endOffset === 'number') range.setEnd(endNode, endOffset); @@ -351,12 +345,12 @@ class TextSelection { } private _setFocus(focusNode: Node, focusOffset: number) { - const selection = this.doc.getSelection(); + const selection = this._doc.getSelection(); if (selection) selection.extend(focusNode, focusOffset); } - private _setCursor() { + private _updateSelection() { const { anchor, focus, @@ -368,7 +362,8 @@ class TextSelection { } = this; if (!anchor || !focus) { - const selection = this.doc.getSelection(); + const selection = this._doc.getSelection(); + if (selection) selection.removeAllRanges(); diff --git a/packages/muya/src/selection/index.ts b/packages/muya/src/selection/index.ts index 1bb757a08b..a9f3f1c6ed 100644 --- a/packages/muya/src/selection/index.ts +++ b/packages/muya/src/selection/index.ts @@ -1,7 +1,7 @@ import type Table from '../block/gfm/table'; import type TableBodyCell from '../block/gfm/table/cell'; import type { Muya } from '../muya'; -import type { ICursor, IImageSelectionData, ISelection, SelectionType } from './types'; +import type { ICursor, IImageSelectionData, ISelection } from './types'; import { getCursorCoords, getCursorYOffset, @@ -10,6 +10,7 @@ import { import ImageSelection from './ImageSelection'; import TableRectSelection from './TableRectSelection'; import TextSelection from './TextSelection'; +import { SelectionType } from './types'; class Selection { static getCursorYOffset(paragraph: HTMLElement) { @@ -37,16 +38,16 @@ class Selection { get type(): SelectionType { if (this._image.selected) - return 'image'; + return SelectionType.Image; if (this._table.hasSelection) - return 'table'; - return 'text'; + return SelectionType.Table; + return SelectionType.Text; } get current(): TextSelection | TableRectSelection | ImageSelection { switch (this.type) { - case 'image': return this._image; - case 'table': return this._table; + case SelectionType.Image: return this._image; + case SelectionType.Table: return this._table; default: return this._text; } } @@ -90,21 +91,20 @@ class Selection { selectImage(data: IImageSelectionData): void { this._image.selected = data; this.muya.editor.activeContentBlock = null; - this.activate('image'); + this.activate(SelectionType.Image); } activate(type: SelectionType): void { - if (type !== 'text') + if (type !== SelectionType.Text) this._text.collapse(); - if (type !== 'table') + if (type !== SelectionType.Table) this._table.clear(); - if (type !== 'image') + if (type !== SelectionType.Image) this._image.clear(); - if (type !== 'text') { + if (type !== SelectionType.Text) { this.muya.eventCenter.emit('selection-change', { kind: type, - selection: this.current, }); } } @@ -128,30 +128,25 @@ class Selection { } selectAll(): void { - const { anchor, focus, isSelectionInSameBlock, anchorBlock, focusBlock, anchorPath } - = this._text; + const { anchor, focus, isSelectionInSameBlock, anchorBlock, focusBlock, anchorPath } = this._text; const tableSelection = this._table; - // Table escalation: - // whole table frozen → clear + select the whole document. - // single cell frozen → select the whole table. if (tableSelection.isWholeTableSelected()) { tableSelection.clear(); this._text.selectAllContent(); return; } + if (tableSelection.isSingleCellSelected()) { const cellBlock = anchorBlock?.closestBlock('table.cell') as TableBodyCell | null; const table = cellBlock?.table ?? null; + if (table) { tableSelection.selectTable(table); return; } } - // Caret / range inside table cells. A 1x1 selection freezes that cell; - // a range across two cells of the same table selects the whole table; - // a range across two different tables is a no-op (no document select). if ( anchorBlock?.blockName === 'table.cell.content' && focusBlock?.blockName === 'table.cell.content' @@ -205,6 +200,7 @@ class Selection { }); return; } + this._text.selectAllContent(); } } diff --git a/packages/muya/src/selection/offsetCursor.ts b/packages/muya/src/selection/offsetCursor.ts index 3088cd8eb5..5eac2feacb 100644 --- a/packages/muya/src/selection/offsetCursor.ts +++ b/packages/muya/src/selection/offsetCursor.ts @@ -140,7 +140,7 @@ export function resolveSentinelCursor(scrollPage: ScrollPage): ICursor | null { let focusOffset = focus.offset; // When both sentinels live in the same block, the second one's recorded - // offset is shifted by the first sentinel's length. Normalise so both + // offset is shifted by the first sentinel's length. Normalize so both // offsets are expressed against the sentinel-free text. if (anchor.block === focus.block) { if (anchorOffset <= focusOffset) @@ -216,7 +216,8 @@ export function injectStateSentinels( state: TState[], selection: ISelection, ): TState[] | null { - const { anchorPath, focusPath } = selection; + const anchorPath = selection.anchor.path; + const focusPath = selection.focus.path; const anchorOffset = selection.anchor.offset; const focusOffset = selection.focus.offset; diff --git a/packages/muya/src/selection/types.ts b/packages/muya/src/selection/types.ts index eb34e01a7b..3fad6f603e 100644 --- a/packages/muya/src/selection/types.ts +++ b/packages/muya/src/selection/types.ts @@ -1,5 +1,6 @@ import type ContentBlock from '../block/base/content'; import type Format from '../block/base/format'; +import type { TBlockPath } from '../block/types'; import type { ImageToken } from '../inlineRenderer/types'; export interface INodeOffset { @@ -11,46 +12,60 @@ export interface ICursor { start?: INodeOffset | null; end?: INodeOffset | null; block?: ContentBlock; - path?: (string | number)[]; + path?: TBlockPath; // The same as TSelection anchor?: INodeOffset | null; focus?: INodeOffset | null; anchorBlock?: ContentBlock; - anchorPath?: (string | number)[]; + anchorPath?: TBlockPath; focusBlock?: ContentBlock; - focusPath?: (string | number)[]; + focusPath?: TBlockPath; isCollapsed?: boolean; isSelectionInSameBlock?: boolean; direction?: string; type?: string; } +// One endpoint of a selection: the offset plus the live block reference and its +// json path. `block` is an in-memory optimization re-resolved from `path` on +// apply, so the history variant below makes it optional. +export interface IAnchorFocusInfo { + offset: number; + block: ContentBlock; + path: TBlockPath; +} + // Only used for selection.getSelection return type. export interface ISelection { - anchor: INodeOffset; - focus: INodeOffset; - anchorBlock: ContentBlock; - anchorPath: (string | number)[]; - focusBlock: ContentBlock; - focusPath: (string | number)[]; + anchor: IAnchorFocusInfo; + focus: IAnchorFocusInfo; isCollapsed: boolean; isSelectionInSameBlock: boolean; direction: string; type: string; } -// An `ISelection` whose live `anchorBlock` / `focusBlock` references are -// optional. The history stacks store selections that may have lost their block -// instances after a serialize/restore round-trip (those references are an -// in-memory optimization re-resolved from `anchorPath` / `focusPath` on apply). -// A full `ISelection` is assignable to this type, so live selections captured -// via `getSelection()` still fit without any cast. -export type IHistorySelection = Omit & { - anchorBlock?: ContentBlock; - focusBlock?: ContentBlock; +// An endpoint whose live `block` reference is optional — used by the history +// stacks, whose selections may have lost their block instances after a +// serialize/restore round-trip (the reference is re-resolved from `path` on +// apply). +export type IHistoryAnchorFocusInfo = Omit & { + block?: ContentBlock; +}; + +// An `ISelection` whose endpoints' live `block` references are optional. A full +// `ISelection` is assignable to this type, so live selections captured via +// `getSelection()` still fit without any cast. +export type IHistorySelection = Omit & { + anchor: IHistoryAnchorFocusInfo; + focus: IHistoryAnchorFocusInfo; }; -export type SelectionType = 'text' | 'table' | 'image'; +export enum SelectionType { + Text = 'text', + Table = 'table', + Image = 'image', +} export interface IImageSelectionData { token: ImageToken; diff --git a/packages/muya/src/state/markdownToHtml.ts b/packages/muya/src/state/markdownToHtml.ts index f91fcf6319..a2c8f07a1c 100644 --- a/packages/muya/src/state/markdownToHtml.ts +++ b/packages/muya/src/state/markdownToHtml.ts @@ -108,7 +108,7 @@ export class MarkdownToHtml { try { if (functionType === 'plantuml') { - const diagram = render.parse(rawCode); + const diagram = render.parse(rawCode, this.muya?.options.plantumlServer); diagramContainer.innerHTML = ''; diagram.insertImgElement(diagramContainer); } diff --git a/packages/muya/src/types.ts b/packages/muya/src/types.ts index 93fbf19cd4..4b47fdf914 100644 --- a/packages/muya/src/types.ts +++ b/packages/muya/src/types.ts @@ -18,6 +18,7 @@ export interface IMuyaOptions { frontmatterType: string; // '-' | '+' | ';' | '{'; mermaidTheme: string; vegaTheme: string; + plantumlServer: string; sequenceTheme: 'hand' | 'simple'; hideQuickInsertHint: boolean; hideLinkPopup: boolean; diff --git a/packages/muya/src/ui/inlineFormatToolbar/index.ts b/packages/muya/src/ui/inlineFormatToolbar/index.ts index da32ca4229..82ba1d687a 100644 --- a/packages/muya/src/ui/inlineFormatToolbar/index.ts +++ b/packages/muya/src/ui/inlineFormatToolbar/index.ts @@ -130,7 +130,8 @@ export class InlineFormatToolbar extends BaseFloat { if (!selection) return; - const { anchorBlock, isSelectionInSameBlock } = selection; + const { anchor, isSelectionInSameBlock } = selection; + const anchorBlock = anchor.block; if (!isSelectionInSameBlock) return; @@ -254,8 +255,8 @@ export class InlineFormatToolbar extends BaseFloat { // Restore selection before formatting selection.setSelection({ - anchor, - focus, + anchor: anchor ?? null, + focus: focus ?? null, anchorBlock: anchorBlock!, anchorPath, focusBlock: focusBlock!, diff --git a/packages/muya/src/ui/paragraphQuickInsertMenu/index.ts b/packages/muya/src/ui/paragraphQuickInsertMenu/index.ts index f04484486f..50a814201d 100644 --- a/packages/muya/src/ui/paragraphQuickInsertMenu/index.ts +++ b/packages/muya/src/ui/paragraphQuickInsertMenu/index.ts @@ -91,8 +91,9 @@ export class ParagraphQuickInsertMenu extends BaseScrollFloat { }); const handleKeydown = (event: Event) => { - const { anchorBlock, isSelectionInSameBlock } - = editor.selection.getSelection() ?? {}; + const selectionResult = editor.selection.getSelection(); + const anchorBlock = selectionResult?.anchor.block; + const isSelectionInSameBlock = selectionResult?.isSelectionInSameBlock; if (isSelectionInSameBlock && anchorBlock instanceof ParagraphContent) { if (anchorBlock.text) return; diff --git a/packages/muya/src/utils/diagram/plantuml/__tests__/index.spec.ts b/packages/muya/src/utils/diagram/plantuml/__tests__/index.spec.ts new file mode 100644 index 0000000000..967aa74ce9 --- /dev/null +++ b/packages/muya/src/utils/diagram/plantuml/__tests__/index.spec.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from 'vitest'; +import Diagram from '../index'; + +const TEST_SOURCE = '@startuml\nA -> B\n@enduml'; + +describe('plantuml Diagram', () => { + it('uses the default server URL when none is provided', () => { + const diagram = Diagram.parse(TEST_SOURCE); + expect(diagram.plantumlServer).toBe('https://www.plantuml.com/plantuml'); + }); + + it('uses the custom server URL when provided', () => { + const customUrl = 'http://localhost:8080/plantuml'; + const diagram = Diagram.parse(TEST_SOURCE, customUrl); + expect(diagram.plantumlServer).toBe(customUrl); + }); + + it('falls back to default when custom URL is empty', () => { + const diagram = Diagram.parse(TEST_SOURCE, ''); + expect(diagram.plantumlServer).toBe('https://www.plantuml.com/plantuml'); + }); + + it('encodes input to a non-empty string', () => { + const diagram = Diagram.parse(TEST_SOURCE); + expect(diagram.encodedInput).toBeTruthy(); + expect(typeof diagram.encodedInput).toBe('string'); + }); +}); diff --git a/packages/muya/src/utils/diagram/plantuml/index.ts b/packages/muya/src/utils/diagram/plantuml/index.ts index 27fb9a643c..f77eecd3b6 100644 --- a/packages/muya/src/utils/diagram/plantuml/index.ts +++ b/packages/muya/src/utils/diagram/plantuml/index.ts @@ -1,14 +1,19 @@ import plantumlEncoder from 'plantuml-encoder'; +const PLANTUML_DEFAULT_URL = 'https://www.plantuml.com/plantuml'; + export default class Diagram { public encodedInput = ''; + public plantumlServer = PLANTUML_DEFAULT_URL; /** * Builds a Diagram object storing the encoded input value */ - static parse(input: string) { + static parse(input: string, plantumlServer?: string) { const diagram = new Diagram(); diagram.encode(input); + if (plantumlServer) + diagram.plantumlServer = plantumlServer; return diagram; } @@ -26,7 +31,6 @@ export default class Diagram { } insertImgElement(container: string | HTMLElement) { - const PLANTUML_URL = 'https://www.plantuml.com/plantuml'; const div = typeof container === 'string' ? document.getElementById(container) @@ -34,7 +38,7 @@ export default class Diagram { if (div === null || !div.tagName) throw new Error(`Invalid container: ${container}`); - const src = `${PLANTUML_URL}/svg/${this.encodedInput}`; + const src = `${this.plantumlServer}/svg/${this.encodedInput}`; div.innerHTML = ``; } diff --git a/packages/muyajs/lib/config/index.js b/packages/muyajs/lib/config/index.js index e2b5c284ef..6f92644ee4 100644 --- a/packages/muyajs/lib/config/index.js +++ b/packages/muyajs/lib/config/index.js @@ -421,6 +421,7 @@ export const MUYA_DEFAULT_OPTION = Object.freeze({ sequenceTheme: 'hand', // hand or simple mermaidTheme: 'default', // dark / forest / default vegaTheme: 'latimes', // excel / ggplot2 / quartz / vox / fivethirtyeight / dark / latimes + plantumlServer: 'https://www.plantuml.com/plantuml', hideQuickInsertHint: false, hideLinkPopup: false, autoCheck: false, diff --git a/packages/muyajs/lib/parser/render/index.js b/packages/muyajs/lib/parser/render/index.js index cc6dfe462a..7197fe12fa 100644 --- a/packages/muyajs/lib/parser/render/index.js +++ b/packages/muyajs/lib/parser/render/index.js @@ -161,7 +161,7 @@ class StateRender { target.innerHTML = '' diagram.drawSVG(target, options) } else if (functionType === 'plantuml') { - const diagram = render.parse(code) + const diagram = render.parse(code, this.muya.options.plantumlServer) target.innerHTML = '' diagram.insertImgElement(target) } else if (functionType === 'vega-lite') { diff --git a/packages/muyajs/lib/parser/render/plantuml.js b/packages/muyajs/lib/parser/render/plantuml.js index 89730b7f37..f21f93fe68 100644 --- a/packages/muyajs/lib/parser/render/plantuml.js +++ b/packages/muyajs/lib/parser/render/plantuml.js @@ -1,7 +1,7 @@ import { deflate } from 'pako' import { toHTML, h } from './snabbdom' -const PLANTUML_URL = 'https://www.plantuml.com/plantuml' +const PLANTUML_DEFAULT_URL = 'https://www.plantuml.com/plantuml' function replaceChar(tableIn, tableOut, char) { const charIndex = tableIn.indexOf(char) @@ -24,10 +24,14 @@ function uint8ArrayToBase64(bytes) { export default class Diagram { encodedInput = '' + plantumlServer = PLANTUML_DEFAULT_URL - static parse(input) { + static parse(input, plantumlServer) { const diagram = new Diagram() diagram.encodedInput = Diagram.encode(input) + if (plantumlServer) { + diagram.plantumlServer = plantumlServer + } return diagram } @@ -51,7 +55,7 @@ export default class Diagram { if (div === null || !div.tagName) { throw new Error('Invalid container: ' + container) } - const src = `${PLANTUML_URL}/svg/~1${this.encodedInput}` + const src = `${this.plantumlServer}/svg/~1${this.encodedInput}` const node = h('img', { attrs: { src } }) div.innerHTML = toHTML(node) } diff --git a/packages/muyajs/lib/utils/exportHtml.js b/packages/muyajs/lib/utils/exportHtml.js index 46d32a20f7..75199795ed 100644 --- a/packages/muyajs/lib/utils/exportHtml.js +++ b/packages/muyajs/lib/utils/exportHtml.js @@ -101,7 +101,7 @@ class ExportHtml { diagram.drawSVG(diagramContainer, options) } if (functionType === 'plantuml') { - const diagram = render.parse(rawCode) + const diagram = render.parse(rawCode, this.muya ? this.muya.options.plantumlServer : undefined) diagramContainer.innerHTML = '' diagram.insertImgElement(diagramContainer) } diff --git a/packages/website/README.md b/packages/website/README.md index 55e35f0abb..a0b15085b1 100644 --- a/packages/website/README.md +++ b/packages/website/README.md @@ -238,7 +238,7 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file - **Main Project**: [MarkText Editor](https://github.com/marktext/marktext) - **Website**: [https://marktext.me](https://marktext.me) -- **Documentation**: [MarkText Docs](https://github.com/marktext/marktext/tree/develop/docs) +- **Documentation**: [MarkText Docs](https://marktext.me/docs) ## 💖 Sponsors diff --git a/packages/website/content/docs/end-user/FAQ.md b/packages/website/content/docs/end-user/FAQ.md index eac2cba1a0..9b78db247f 100644 --- a/packages/website/content/docs/end-user/FAQ.md +++ b/packages/website/content/docs/end-user/FAQ.md @@ -18,11 +18,11 @@ MarkText is a pure markdown editor without feature such as knowledge management ### Where can I find documentation? -Documentation is currently under development. +The full documentation is available on the MarkText website: -- [End-user documentation](https://github.com/marktext/marktext/blob/develop/docs/README.md) +- [End-user documentation](../README.md) -- [Developer documentation](https://github.com/marktext/marktext/blob/develop/docs/dev/README.md) +- [Developer documentation](../dev/README.md) ### Can I run a portable version of MarkText? diff --git a/packages/website/src/lib/docs-nav.ts b/packages/website/src/lib/docs-nav.ts index 7a04732975..7959cc3e72 100644 --- a/packages/website/src/lib/docs-nav.ts +++ b/packages/website/src/lib/docs-nav.ts @@ -32,6 +32,7 @@ export const DOC_TABS: DocTab[] = [ pages: [ { slug: ['introduction'], title: 'Introduction', file: 'README.md', hint: 'Overview & getting started' }, { slug: ['installation'], title: 'Installation', file: 'end-user/INSTALLATION.md', hint: 'Download & install on every platform' }, + { slug: ['linux'], title: 'Linux notes', file: 'end-user/LINUX.md', hint: 'Distro-specific install tips & quirks' }, { slug: ['basics'], title: 'Basics', file: 'end-user/BASICS.md', hint: 'The interface, files & tabs' }, { slug: ['editing'], title: 'Editing in depth', file: 'end-user/EDITING.md', hint: 'Shortcuts, format bar & find/replace' }, { slug: ['spelling'], title: 'Spelling', file: 'end-user/SPELLING.md', hint: 'Spell checker & dictionaries' }, @@ -43,6 +44,9 @@ export const DOC_TABS: DocTab[] = [ pages: [ { slug: ['preferences'], title: 'Preferences', file: 'end-user/PREFERENCES.md', hint: 'App settings reference' }, { slug: ['key-bindings'], title: 'Key bindings', file: 'end-user/KEYBINDINGS.md', hint: 'Default & custom shortcuts' }, + { slug: ['key-bindings-macos'], title: 'Key bindings (macOS)', file: 'end-user/KEYBINDINGS_OSX.md', hint: 'Default shortcuts on macOS' }, + { slug: ['key-bindings-linux'], title: 'Key bindings (Linux)', file: 'end-user/KEYBINDINGS_LINUX.md', hint: 'Default shortcuts on Linux' }, + { slug: ['key-bindings-windows'], title: 'Key bindings (Windows)', file: 'end-user/KEYBINDINGS_WINDOWS.md', hint: 'Default shortcuts on Windows' }, { slug: ['application-data-directory'], title: 'Application data directory', file: 'end-user/APPLICATION_DATA_DIRECTORY.md', hint: 'Where MarkText stores user data' }, { slug: ['environment-variables'], title: 'Environment variables', file: 'end-user/ENVIRONMENT.md', hint: 'Runtime environment overrides' }, { slug: ['cli'], title: 'Command line interface', file: 'end-user/CLI.md', hint: 'Flags, switches, exit codes' } @@ -54,6 +58,7 @@ export const DOC_TABS: DocTab[] = [ { slug: ['export'], title: 'Export a document', file: 'end-user/EXPORT.md', hint: 'PDF, HTML, image export' }, { slug: ['themes'], title: 'Themes', file: 'end-user/THEMES.md', hint: 'Built-in & custom UI themes' }, { slug: ['export-themes'], title: 'Themes for exporting', file: 'end-user/EXPORT_THEMES.md', hint: 'Style your exported HTML' }, + { slug: ['images'], title: 'Image support', file: 'end-user/IMAGES.md', hint: 'Copy images to a local folder' }, { slug: ['image-uploader'], title: 'Image uploader configuration', file: 'end-user/IMAGE_UPLOADER_CONFIGRATION.md', hint: 'Cloud image hosts' } ] },