diff --git a/.gitignore b/.gitignore index c5fb43d..b39e64c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ + .DS_Store .idea diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..5001e91 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "python.testing.unittestArgs": [ + "-v", + "-s", + "./test", + "-p", + "*_test.py" + ], + "python.testing.pytestEnabled": false, + "python.testing.unittestEnabled": true +} \ No newline at end of file diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..9067811 --- /dev/null +++ b/Pipfile @@ -0,0 +1,19 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +pillow = "*" +matplotlib = "*" +requests = "*" +opencv-python = "*" +numpy = "*" +tqdm = "*" +typing-extensions = "*" + +[dev-packages] + +[requires] +python_version = "3.9" +python_full_version = "3.9.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..47c24d8 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,519 @@ +{ + "_meta": { + "hash": { + "sha256": "174e9bd93434f1fd21e2e9e92119a6f30ce9777aaf6bd06a443c2f5bef0b5cc3" + }, + "pipfile-spec": 6, + "requires": { + "python_full_version": "3.9.7", + "python_version": "3.9" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082", + "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9" + ], + "markers": "python_version >= '3.6'", + "version": "==2023.7.22" + }, + "charset-normalizer": { + "hashes": [ + "sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96", + "sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c", + "sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710", + "sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706", + "sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020", + "sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252", + "sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad", + "sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329", + "sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a", + "sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f", + "sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6", + "sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4", + "sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a", + "sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46", + "sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2", + "sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23", + "sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace", + "sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd", + "sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982", + "sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10", + "sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2", + "sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea", + "sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09", + "sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5", + "sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149", + "sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489", + "sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9", + "sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80", + "sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592", + "sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3", + "sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6", + "sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed", + "sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c", + "sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200", + "sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a", + "sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e", + "sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d", + "sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6", + "sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623", + "sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669", + "sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3", + "sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa", + "sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9", + "sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2", + "sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f", + "sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1", + "sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4", + "sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a", + "sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8", + "sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3", + "sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029", + "sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f", + "sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959", + "sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22", + "sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7", + "sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952", + "sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346", + "sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e", + "sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d", + "sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299", + "sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd", + "sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a", + "sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3", + "sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037", + "sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94", + "sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c", + "sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858", + "sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a", + "sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449", + "sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c", + "sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918", + "sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1", + "sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c", + "sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac", + "sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa" + ], + "markers": "python_full_version >= '3.7.0'", + "version": "==3.2.0" + }, + "colorama": { + "hashes": [ + "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", + "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" + ], + "markers": "platform_system == 'Windows'", + "version": "==0.4.6" + }, + "contourpy": { + "hashes": [ + "sha256:052cc634bf903c604ef1a00a5aa093c54f81a2612faedaa43295809ffdde885e", + "sha256:084eaa568400cfaf7179b847ac871582199b1b44d5699198e9602ecbbb5f6104", + "sha256:0b6616375d7de55797d7a66ee7d087efe27f03d336c27cf1f32c02b8c1a5ac70", + "sha256:0b7b04ed0961647691cfe5d82115dd072af7ce8846d31a5fac6c142dcce8b882", + "sha256:143dde50520a9f90e4a2703f367cf8ec96a73042b72e68fcd184e1279962eb6f", + "sha256:17cfaf5ec9862bc93af1ec1f302457371c34e688fbd381f4035a06cd47324f48", + "sha256:181cbace49874f4358e2929aaf7ba84006acb76694102e88dd15af861996c16e", + "sha256:189ceb1525eb0655ab8487a9a9c41f42a73ba52d6789754788d1883fb06b2d8a", + "sha256:18a64814ae7bce73925131381603fff0116e2df25230dfc80d6d690aa6e20b37", + "sha256:1f0cbd657e9bde94cd0e33aa7df94fb73c1ab7799378d3b3f902eb8eb2e04a3a", + "sha256:1f795597073b09d631782e7245016a4323cf1cf0b4e06eef7ea6627e06a37ff2", + "sha256:25ae46595e22f93592d39a7eac3d638cda552c3e1160255258b695f7b58e5655", + "sha256:27bc79200c742f9746d7dd51a734ee326a292d77e7d94c8af6e08d1e6c15d545", + "sha256:2b836d22bd2c7bb2700348e4521b25e077255ebb6ab68e351ab5aa91ca27e027", + "sha256:30f511c05fab7f12e0b1b7730ebdc2ec8deedcfb505bc27eb570ff47c51a8f15", + "sha256:317267d915490d1e84577924bd61ba71bf8681a30e0d6c545f577363157e5e94", + "sha256:397b0ac8a12880412da3551a8cb5a187d3298a72802b45a3bd1805e204ad8439", + "sha256:438ba416d02f82b692e371858143970ed2eb6337d9cdbbede0d8ad9f3d7dd17d", + "sha256:53cc3a40635abedbec7f1bde60f8c189c49e84ac180c665f2cd7c162cc454baa", + "sha256:5d123a5bc63cd34c27ff9c7ac1cd978909e9c71da12e05be0231c608048bb2ae", + "sha256:62013a2cf68abc80dadfd2307299bfa8f5aa0dcaec5b2954caeb5fa094171103", + "sha256:89f06eff3ce2f4b3eb24c1055a26981bffe4e7264acd86f15b97e40530b794bc", + "sha256:90c81f22b4f572f8a2110b0b741bb64e5a6427e0a198b2cdc1fbaf85f352a3aa", + "sha256:911ff4fd53e26b019f898f32db0d4956c9d227d51338fb3b03ec72ff0084ee5f", + "sha256:9382a1c0bc46230fb881c36229bfa23d8c303b889b788b939365578d762b5c18", + "sha256:9f2931ed4741f98f74b410b16e5213f71dcccee67518970c42f64153ea9313b9", + "sha256:a67259c2b493b00e5a4d0f7bfae51fb4b3371395e47d079a4446e9b0f4d70e76", + "sha256:a698c6a7a432789e587168573a864a7ea374c6be8d4f31f9d87c001d5a843493", + "sha256:bc00bb4225d57bff7ebb634646c0ee2a1298402ec10a5fe7af79df9a51c1bfd9", + "sha256:bcb41692aa09aeb19c7c213411854402f29f6613845ad2453d30bf421fe68fed", + "sha256:d4f26b25b4f86087e7d75e63212756c38546e70f2a92d2be44f80114826e1cd4", + "sha256:d551f3a442655f3dcc1285723f9acd646ca5858834efeab4598d706206b09c9f", + "sha256:dffcc2ddec1782dd2f2ce1ef16f070861af4fb78c69862ce0aab801495dda6a3", + "sha256:e53046c3863828d21d531cc3b53786e6580eb1ba02477e8681009b6aa0870b21", + "sha256:e5cec36c5090e75a9ac9dbd0ff4a8cf7cecd60f1b6dc23a374c7d980a1cd710e", + "sha256:e7a117ce7df5a938fe035cad481b0189049e8d92433b4b33aa7fc609344aafa1", + "sha256:e94bef2580e25b5fdb183bf98a2faa2adc5b638736b2c0a4da98691da641316a", + "sha256:ed614aea8462735e7d70141374bd7650afd1c3f3cb0c2dbbcbe44e14331bf002", + "sha256:fb3b7d9e6243bfa1efb93ccfe64ec610d85cfe5aec2c25f97fbbd2e58b531256" + ], + "markers": "python_version >= '3.8'", + "version": "==1.1.0" + }, + "cycler": { + "hashes": [ + "sha256:3a27e95f763a428a739d2add979fa7494c912a32c17c4c38c4d5f082cad165a3", + "sha256:9c87405839a19696e837b3b818fed3f5f69f16f1eec1a1ad77e043dcea9c772f" + ], + "markers": "python_version >= '3.6'", + "version": "==0.11.0" + }, + "fonttools": { + "hashes": [ + "sha256:0eb79a2da5eb6457a6f8ab904838454accc7d4cccdaff1fd2bd3a0679ea33d64", + "sha256:113337c2d29665839b7d90b39f99b3cac731f72a0eda9306165a305c7c31d341", + "sha256:12a7c247d1b946829bfa2f331107a629ea77dc5391dfd34fdcd78efa61f354ca", + "sha256:179737095eb98332a2744e8f12037b2977f22948cf23ff96656928923ddf560a", + "sha256:19b7db825c8adee96fac0692e6e1ecd858cae9affb3b4812cdb9d934a898b29e", + "sha256:37983b6bdab42c501202500a2be3a572f50d4efe3237e0686ee9d5f794d76b35", + "sha256:3a35981d90feebeaef05e46e33e6b9e5b5e618504672ca9cd0ff96b171e4bfff", + "sha256:46a0ec8adbc6ff13494eb0c9c2e643b6f009ce7320cf640de106fb614e4d4360", + "sha256:4aa79366e442dbca6e2c8595645a3a605d9eeabdb7a094d745ed6106816bef5d", + "sha256:515607ec756d7865f23070682622c49d922901943697871fc292277cf1e71967", + "sha256:53eb5091ddc8b1199330bb7b4a8a2e7995ad5d43376cadce84523d8223ef3136", + "sha256:5d18fc642fd0ac29236ff88ecfccff229ec0386090a839dd3f1162e9a7944a40", + "sha256:5fb289b7a815638a7613d46bcf324c9106804725b2bb8ad913c12b6958ffc4ec", + "sha256:62f481ac772fd68901573956231aea3e4b1ad87b9b1089a61613a91e2b50bb9b", + "sha256:689508b918332fb40ce117131633647731d098b1b10d092234aa959b4251add5", + "sha256:68a02bbe020dc22ee0540e040117535f06df9358106d3775e8817d826047f3fd", + "sha256:6ed2662a3d9c832afa36405f8748c250be94ae5dfc5283d668308391f2102861", + "sha256:7286aed4ea271df9eab8d7a9b29e507094b51397812f7ce051ecd77915a6e26b", + "sha256:7cc7d685b8eeca7ae69dc6416833fbfea61660684b7089bca666067cb2937dcf", + "sha256:8708b98c278012ad267ee8a7433baeb809948855e81922878118464b274c909d", + "sha256:9398f244e28e0596e2ee6024f808b06060109e33ed38dcc9bded452fd9bbb853", + "sha256:9e36344e48af3e3bde867a1ca54f97c308735dd8697005c2d24a86054a114a71", + "sha256:a398bdadb055f8de69f62b0fc70625f7cbdab436bbb31eef5816e28cab083ee8", + "sha256:acb47f6f8680de24c1ab65ebde39dd035768e2a9b571a07c7b8da95f6c8815fd", + "sha256:be24fcb80493b2c94eae21df70017351851652a37de514de553435b256b2f249", + "sha256:c391cd5af88aacaf41dd7cfb96eeedfad297b5899a39e12f4c2c3706d0a3329d", + "sha256:c95b0724a6deea2c8c5d3222191783ced0a2f09bd6d33f93e563f6f1a4b3b3a4", + "sha256:c9b1ce7a45978b821a06d375b83763b27a3a5e8a2e4570b3065abad240a18760", + "sha256:db372213d39fa33af667c2aa586a0c1235e88e9c850f5dd5c8e1f17515861868", + "sha256:db55cbaea02a20b49fefbd8e9d62bd481aaabe1f2301dabc575acc6b358874fa", + "sha256:ed1a13a27f59d1fc1920394a7f596792e9d546c9ca5a044419dca70c37815d7c", + "sha256:f2b82f46917d8722e6b5eafeefb4fb585d23babd15d8246c664cd88a5bddd19c", + "sha256:f2f806990160d1ce42d287aa419df3ffc42dfefe60d473695fb048355fe0c6a0", + "sha256:f720fa82a11c0f9042376fd509b5ed88dab7e3cd602eee63a1af08883b37342b" + ], + "markers": "python_version >= '3.8'", + "version": "==4.42.1" + }, + "idna": { + "hashes": [ + "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", + "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" + ], + "markers": "python_version >= '3.5'", + "version": "==3.4" + }, + "importlib-resources": { + "hashes": [ + "sha256:134832a506243891221b88b4ae1213327eea96ceb4e407a00d790bb0626f45cf", + "sha256:4359457e42708462b9626a04657c6208ad799ceb41e5c58c57ffa0e6a098a5d4" + ], + "markers": "python_version < '3.10'", + "version": "==6.0.1" + }, + "kiwisolver": { + "hashes": [ + "sha256:02f79693ec433cb4b5f51694e8477ae83b3205768a6fb48ffba60549080e295b", + "sha256:03baab2d6b4a54ddbb43bba1a3a2d1627e82d205c5cf8f4c924dc49284b87166", + "sha256:1041feb4cda8708ce73bb4dcb9ce1ccf49d553bf87c3954bdfa46f0c3f77252c", + "sha256:10ee06759482c78bdb864f4109886dff7b8a56529bc1609d4f1112b93fe6423c", + "sha256:1d1573129aa0fd901076e2bfb4275a35f5b7aa60fbfb984499d661ec950320b0", + "sha256:283dffbf061a4ec60391d51e6155e372a1f7a4f5b15d59c8505339454f8989e4", + "sha256:28bc5b299f48150b5f822ce68624e445040595a4ac3d59251703779836eceff9", + "sha256:2a66fdfb34e05b705620dd567f5a03f239a088d5a3f321e7b6ac3239d22aa286", + "sha256:2e307eb9bd99801f82789b44bb45e9f541961831c7311521b13a6c85afc09767", + "sha256:2e407cb4bd5a13984a6c2c0fe1845e4e41e96f183e5e5cd4d77a857d9693494c", + "sha256:2f5e60fabb7343a836360c4f0919b8cd0d6dbf08ad2ca6b9cf90bf0c76a3c4f6", + "sha256:36dafec3d6d6088d34e2de6b85f9d8e2324eb734162fba59d2ba9ed7a2043d5b", + "sha256:3fe20f63c9ecee44560d0e7f116b3a747a5d7203376abeea292ab3152334d004", + "sha256:41dae968a94b1ef1897cb322b39360a0812661dba7c682aa45098eb8e193dbdf", + "sha256:4bd472dbe5e136f96a4b18f295d159d7f26fd399136f5b17b08c4e5f498cd494", + "sha256:4ea39b0ccc4f5d803e3337dd46bcce60b702be4d86fd0b3d7531ef10fd99a1ac", + "sha256:5853eb494c71e267912275e5586fe281444eb5e722de4e131cddf9d442615626", + "sha256:5bce61af018b0cb2055e0e72e7d65290d822d3feee430b7b8203d8a855e78766", + "sha256:6295ecd49304dcf3bfbfa45d9a081c96509e95f4b9d0eb7ee4ec0530c4a96514", + "sha256:62ac9cc684da4cf1778d07a89bf5f81b35834cb96ca523d3a7fb32509380cbf6", + "sha256:70e7c2e7b750585569564e2e5ca9845acfaa5da56ac46df68414f29fea97be9f", + "sha256:7577c1987baa3adc4b3c62c33bd1118c3ef5c8ddef36f0f2c950ae0b199e100d", + "sha256:75facbe9606748f43428fc91a43edb46c7ff68889b91fa31f53b58894503a191", + "sha256:787518a6789009c159453da4d6b683f468ef7a65bbde796bcea803ccf191058d", + "sha256:78d6601aed50c74e0ef02f4204da1816147a6d3fbdc8b3872d263338a9052c51", + "sha256:7c43e1e1206cd421cd92e6b3280d4385d41d7166b3ed577ac20444b6995a445f", + "sha256:81e38381b782cc7e1e46c4e14cd997ee6040768101aefc8fa3c24a4cc58e98f8", + "sha256:841293b17ad704d70c578f1f0013c890e219952169ce8a24ebc063eecf775454", + "sha256:872b8ca05c40d309ed13eb2e582cab0c5a05e81e987ab9c521bf05ad1d5cf5cb", + "sha256:877272cf6b4b7e94c9614f9b10140e198d2186363728ed0f701c6eee1baec1da", + "sha256:8c808594c88a025d4e322d5bb549282c93c8e1ba71b790f539567932722d7bd8", + "sha256:8ed58b8acf29798b036d347791141767ccf65eee7f26bde03a71c944449e53de", + "sha256:91672bacaa030f92fc2f43b620d7b337fd9a5af28b0d6ed3f77afc43c4a64b5a", + "sha256:968f44fdbf6dd757d12920d63b566eeb4d5b395fd2d00d29d7ef00a00582aac9", + "sha256:9f85003f5dfa867e86d53fac6f7e6f30c045673fa27b603c397753bebadc3008", + "sha256:a553dadda40fef6bfa1456dc4be49b113aa92c2a9a9e8711e955618cd69622e3", + "sha256:a68b62a02953b9841730db7797422f983935aeefceb1679f0fc85cbfbd311c32", + "sha256:abbe9fa13da955feb8202e215c4018f4bb57469b1b78c7a4c5c7b93001699938", + "sha256:ad881edc7ccb9d65b0224f4e4d05a1e85cf62d73aab798943df6d48ab0cd79a1", + "sha256:b1792d939ec70abe76f5054d3f36ed5656021dcad1322d1cc996d4e54165cef9", + "sha256:b428ef021242344340460fa4c9185d0b1f66fbdbfecc6c63eff4b7c29fad429d", + "sha256:b533558eae785e33e8c148a8d9921692a9fe5aa516efbdff8606e7d87b9d5824", + "sha256:ba59c92039ec0a66103b1d5fe588fa546373587a7d68f5c96f743c3396afc04b", + "sha256:bc8d3bd6c72b2dd9decf16ce70e20abcb3274ba01b4e1c96031e0c4067d1e7cd", + "sha256:bc9db8a3efb3e403e4ecc6cd9489ea2bac94244f80c78e27c31dcc00d2790ac2", + "sha256:bf7d9fce9bcc4752ca4a1b80aabd38f6d19009ea5cbda0e0856983cf6d0023f5", + "sha256:c2dbb44c3f7e6c4d3487b31037b1bdbf424d97687c1747ce4ff2895795c9bf69", + "sha256:c79ebe8f3676a4c6630fd3f777f3cfecf9289666c84e775a67d1d358578dc2e3", + "sha256:c97528e64cb9ebeff9701e7938653a9951922f2a38bd847787d4a8e498cc83ae", + "sha256:d0611a0a2a518464c05ddd5a3a1a0e856ccc10e67079bb17f265ad19ab3c7597", + "sha256:d06adcfa62a4431d404c31216f0f8ac97397d799cd53800e9d3efc2fbb3cf14e", + "sha256:d41997519fcba4a1e46eb4a2fe31bc12f0ff957b2b81bac28db24744f333e955", + "sha256:d5b61785a9ce44e5a4b880272baa7cf6c8f48a5180c3e81c59553ba0cb0821ca", + "sha256:da152d8cdcab0e56e4f45eb08b9aea6455845ec83172092f09b0e077ece2cf7a", + "sha256:da7e547706e69e45d95e116e6939488d62174e033b763ab1496b4c29b76fabea", + "sha256:db5283d90da4174865d520e7366801a93777201e91e79bacbac6e6927cbceede", + "sha256:db608a6757adabb32f1cfe6066e39b3706d8c3aa69bbc353a5b61edad36a5cb4", + "sha256:e0ea21f66820452a3f5d1655f8704a60d66ba1191359b96541eaf457710a5fc6", + "sha256:e7da3fec7408813a7cebc9e4ec55afed2d0fd65c4754bc376bf03498d4e92686", + "sha256:e92a513161077b53447160b9bd8f522edfbed4bd9759e4c18ab05d7ef7e49408", + "sha256:ecb1fa0db7bf4cff9dac752abb19505a233c7f16684c5826d1f11ebd9472b871", + "sha256:efda5fc8cc1c61e4f639b8067d118e742b812c930f708e6667a5ce0d13499e29", + "sha256:f0a1dbdb5ecbef0d34eb77e56fcb3e95bbd7e50835d9782a45df81cc46949750", + "sha256:f0a71d85ecdd570ded8ac3d1c0f480842f49a40beb423bb8014539a9f32a5897", + "sha256:f4f270de01dd3e129a72efad823da90cc4d6aafb64c410c9033aba70db9f1ff0", + "sha256:f6cb459eea32a4e2cf18ba5fcece2dbdf496384413bc1bae15583f19e567f3b2", + "sha256:f8ad8285b01b0d4695102546b342b493b3ccc6781fc28c8c6a1bb63e95d22f09", + "sha256:f9f39e2f049db33a908319cf46624a569b36983c7c78318e9726a4cb8923b26c" + ], + "markers": "python_version >= '3.7'", + "version": "==1.4.4" + }, + "matplotlib": { + "hashes": [ + "sha256:070f8dddd1f5939e60aacb8fa08f19551f4b0140fab16a3669d5cd6e9cb28fc8", + "sha256:0c3cca3e842b11b55b52c6fb8bd6a4088693829acbfcdb3e815fa9b7d5c92c1b", + "sha256:0f506a1776ee94f9e131af1ac6efa6e5bc7cb606a3e389b0ccb6e657f60bb676", + "sha256:12f01b92ecd518e0697da4d97d163b2b3aa55eb3eb4e2c98235b3396d7dad55f", + "sha256:152ee0b569a37630d8628534c628456b28686e085d51394da6b71ef84c4da201", + "sha256:1c308b255efb9b06b23874236ec0f10f026673ad6515f602027cc8ac7805352d", + "sha256:1cd120fca3407a225168238b790bd5c528f0fafde6172b140a2f3ab7a4ea63e9", + "sha256:20f844d6be031948148ba49605c8b96dfe7d3711d1b63592830d650622458c11", + "sha256:23fb1750934e5f0128f9423db27c474aa32534cec21f7b2153262b066a581fd1", + "sha256:2699f7e73a76d4c110f4f25be9d2496d6ab4f17345307738557d345f099e07de", + "sha256:26bede320d77e469fdf1bde212de0ec889169b04f7f1179b8930d66f82b30cbc", + "sha256:2ecb5be2b2815431c81dc115667e33da0f5a1bcf6143980d180d09a717c4a12e", + "sha256:2f8e4a49493add46ad4a8c92f63e19d548b2b6ebbed75c6b4c7f46f57d36cdd1", + "sha256:305e3da477dc8607336ba10bac96986d6308d614706cae2efe7d3ffa60465b24", + "sha256:30e1409b857aa8a747c5d4f85f63a79e479835f8dffc52992ac1f3f25837b544", + "sha256:318c89edde72ff95d8df67d82aca03861240512994a597a435a1011ba18dbc7f", + "sha256:35d74ebdb3f71f112b36c2629cf32323adfbf42679e2751252acd468f5001c07", + "sha256:50e0a55ec74bf2d7a0ebf50ac580a209582c2dd0f7ab51bc270f1b4a0027454e", + "sha256:5dea00b62d28654b71ca92463656d80646675628d0828e08a5f3b57e12869e13", + "sha256:60c521e21031632aa0d87ca5ba0c1c05f3daacadb34c093585a0be6780f698e4", + "sha256:6515e878f91894c2e4340d81f0911857998ccaf04dbc1bba781e3d89cbf70608", + "sha256:6d2ff3c984b8a569bc1383cd468fc06b70d7b59d5c2854ca39f1436ae8394117", + "sha256:71667eb2ccca4c3537d9414b1bc00554cb7f91527c17ee4ec38027201f8f1603", + "sha256:717157e61b3a71d3d26ad4e1770dc85156c9af435659a25ee6407dc866cb258d", + "sha256:71f7a8c6b124e904db550f5b9fe483d28b896d4135e45c4ea381ad3b8a0e3256", + "sha256:936bba394682049919dda062d33435b3be211dc3dcaa011e09634f060ec878b2", + "sha256:a1733b8e84e7e40a9853e505fe68cc54339f97273bdfe6f3ed980095f769ddc7", + "sha256:a2c1590b90aa7bd741b54c62b78de05d4186271e34e2377e0289d943b3522273", + "sha256:a7e28d6396563955f7af437894a36bf2b279462239a41028323e04b85179058b", + "sha256:a8035ba590658bae7562786c9cc6ea1a84aa49d3afab157e414c9e2ea74f496d", + "sha256:a8cdb91dddb04436bd2f098b8fdf4b81352e68cf4d2c6756fcc414791076569b", + "sha256:ac60daa1dc83e8821eed155796b0f7888b6b916cf61d620a4ddd8200ac70cd64", + "sha256:af4860132c8c05261a5f5f8467f1b269bf1c7c23902d75f2be57c4a7f2394b3e", + "sha256:bc221ffbc2150458b1cd71cdd9ddd5bb37962b036e41b8be258280b5b01da1dd", + "sha256:ce55289d5659b5b12b3db4dc9b7075b70cef5631e56530f14b2945e8836f2d20", + "sha256:d9881356dc48e58910c53af82b57183879129fa30492be69058c5b0d9fddf391", + "sha256:dbcf59334ff645e6a67cd5f78b4b2cdb76384cdf587fa0d2dc85f634a72e1a3e", + "sha256:ebf577c7a6744e9e1bd3fee45fc74a02710b214f94e2bde344912d85e0c9af7c", + "sha256:f081c03f413f59390a80b3e351cc2b2ea0205839714dbc364519bcf51f4b56ca", + "sha256:fdbb46fad4fb47443b5b8ac76904b2e7a66556844f33370861b4788db0f8816a", + "sha256:fdcd28360dbb6203fb5219b1a5658df226ac9bebc2542a9e8f457de959d713d0" + ], + "index": "pypi", + "version": "==3.7.2" + }, + "numpy": { + "hashes": [ + "sha256:0d60fbae8e0019865fc4784745814cff1c421df5afee233db6d88ab4f14655a2", + "sha256:1a1329e26f46230bf77b02cc19e900db9b52f398d6722ca853349a782d4cff55", + "sha256:1b9735c27cea5d995496f46a8b1cd7b408b3f34b6d50459d9ac8fe3a20cc17bf", + "sha256:2792d23d62ec51e50ce4d4b7d73de8f67a2fd3ea710dcbc8563a51a03fb07b01", + "sha256:3e0746410e73384e70d286f93abf2520035250aad8c5714240b0492a7302fdca", + "sha256:4c3abc71e8b6edba80a01a52e66d83c5d14433cbcd26a40c329ec7ed09f37901", + "sha256:5883c06bb92f2e6c8181df7b39971a5fb436288db58b5a1c3967702d4278691d", + "sha256:5c97325a0ba6f9d041feb9390924614b60b99209a71a69c876f71052521d42a4", + "sha256:60e7f0f7f6d0eee8364b9a6304c2845b9c491ac706048c7e8cf47b83123b8dbf", + "sha256:76b4115d42a7dfc5d485d358728cdd8719be33cc5ec6ec08632a5d6fca2ed380", + "sha256:7dc869c0c75988e1c693d0e2d5b26034644399dd929bc049db55395b1379e044", + "sha256:834b386f2b8210dca38c71a6e0f4fd6922f7d3fcff935dbe3a570945acb1b545", + "sha256:8b77775f4b7df768967a7c8b3567e309f617dd5e99aeb886fa14dc1a0791141f", + "sha256:90319e4f002795ccfc9050110bbbaa16c944b1c37c0baeea43c5fb881693ae1f", + "sha256:b79e513d7aac42ae918db3ad1341a015488530d0bb2a6abcbdd10a3a829ccfd3", + "sha256:bb33d5a1cf360304754913a350edda36d5b8c5331a8237268c48f91253c3a364", + "sha256:bec1e7213c7cb00d67093247f8c4db156fd03075f49876957dca4711306d39c9", + "sha256:c5462d19336db4560041517dbb7759c21d181a67cb01b36ca109b2ae37d32418", + "sha256:c5652ea24d33585ea39eb6a6a15dac87a1206a692719ff45d53c5282e66d4a8f", + "sha256:d7806500e4f5bdd04095e849265e55de20d8cc4b661b038957354327f6d9b295", + "sha256:db3ccc4e37a6873045580d413fe79b68e47a681af8db2e046f1dacfa11f86eb3", + "sha256:dfe4a913e29b418d096e696ddd422d8a5d13ffba4ea91f9f60440a3b759b0187", + "sha256:eb942bfb6f84df5ce05dbf4b46673ffed0d3da59f13635ea9b926af3deb76926", + "sha256:f08f2e037bba04e707eebf4bc934f1972a315c883a9e0ebfa8a7756eabf9e357", + "sha256:fd608e19c8d7c55021dffd43bfe5492fab8cc105cc8986f813f8c3c048b38760" + ], + "index": "pypi", + "version": "==1.25.2" + }, + "opencv-python": { + "hashes": [ + "sha256:48eb3121d809a873086d6677565e3ac963e6946110d13cd115533fa70e2aa2eb", + "sha256:56d84c43ce800938b9b1ec74b33942b2edbcef3f70c2754eb9bfe5dff1ee3ace", + "sha256:67bce4b9aad307c98a9a07c6afb7de3a4e823c1f4991d6d8e88e229e7dfeee59", + "sha256:93871871b1c9d6b125cddd45b0638a2fa01ee9fd37f5e428823f750e404f2f15", + "sha256:9bcb4944211acf13742dbfd9d3a11dc4e36353ffa1746f2c7dcd6a01c32d1376", + "sha256:b2349dc9f97ed6c9ba163d0a7a24bcef9695a3e216cd143e92f1b9659c5d9a49", + "sha256:ba32cfa75a806abd68249699d34420737d27b5678553387fc5768747a6492147" + ], + "index": "pypi", + "version": "==4.8.0.76" + }, + "packaging": { + "hashes": [ + "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61", + "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f" + ], + "markers": "python_version >= '3.7'", + "version": "==23.1" + }, + "pillow": { + "hashes": [ + "sha256:00e65f5e822decd501e374b0650146063fbb30a7264b4d2744bdd7b913e0cab5", + "sha256:040586f7d37b34547153fa383f7f9aed68b738992380ac911447bb78f2abe530", + "sha256:0b6eb5502f45a60a3f411c63187db83a3d3107887ad0d036c13ce836f8a36f1d", + "sha256:1ce91b6ec08d866b14413d3f0bbdea7e24dfdc8e59f562bb77bc3fe60b6144ca", + "sha256:1f62406a884ae75fb2f818694469519fb685cc7eaff05d3451a9ebe55c646891", + "sha256:22c10cc517668d44b211717fd9775799ccec4124b9a7f7b3635fc5386e584992", + "sha256:3400aae60685b06bb96f99a21e1ada7bc7a413d5f49bce739828ecd9391bb8f7", + "sha256:349930d6e9c685c089284b013478d6f76e3a534e36ddfa912cde493f235372f3", + "sha256:368ab3dfb5f49e312231b6f27b8820c823652b7cd29cfbd34090565a015e99ba", + "sha256:38250a349b6b390ee6047a62c086d3817ac69022c127f8a5dc058c31ccef17f3", + "sha256:3a684105f7c32488f7153905a4e3015a3b6c7182e106fe3c37fbb5ef3e6994c3", + "sha256:3a82c40d706d9aa9734289740ce26460a11aeec2d9c79b7af87bb35f0073c12f", + "sha256:3b08d4cc24f471b2c8ca24ec060abf4bebc6b144cb89cba638c720546b1cf538", + "sha256:3ed64f9ca2f0a95411e88a4efbd7a29e5ce2cea36072c53dd9d26d9c76f753b3", + "sha256:3f07ea8d2f827d7d2a49ecf1639ec02d75ffd1b88dcc5b3a61bbb37a8759ad8d", + "sha256:520f2a520dc040512699f20fa1c363eed506e94248d71f85412b625026f6142c", + "sha256:5c6e3df6bdd396749bafd45314871b3d0af81ff935b2d188385e970052091017", + "sha256:608bfdee0d57cf297d32bcbb3c728dc1da0907519d1784962c5f0c68bb93e5a3", + "sha256:685ac03cc4ed5ebc15ad5c23bc555d68a87777586d970c2c3e216619a5476223", + "sha256:76de421f9c326da8f43d690110f0e79fe3ad1e54be811545d7d91898b4c8493e", + "sha256:76edb0a1fa2b4745fb0c99fb9fb98f8b180a1bbceb8be49b087e0b21867e77d3", + "sha256:7be600823e4c8631b74e4a0d38384c73f680e6105a7d3c6824fcf226c178c7e6", + "sha256:81ff539a12457809666fef6624684c008e00ff6bf455b4b89fd00a140eecd640", + "sha256:88af2003543cc40c80f6fca01411892ec52b11021b3dc22ec3bc9d5afd1c5334", + "sha256:8c11160913e3dd06c8ffdb5f233a4f254cb449f4dfc0f8f4549eda9e542c93d1", + "sha256:8f8182b523b2289f7c415f589118228d30ac8c355baa2f3194ced084dac2dbba", + "sha256:9211e7ad69d7c9401cfc0e23d49b69ca65ddd898976d660a2fa5904e3d7a9baa", + "sha256:92be919bbc9f7d09f7ae343c38f5bb21c973d2576c1d45600fce4b74bafa7ac0", + "sha256:9c82b5b3e043c7af0d95792d0d20ccf68f61a1fec6b3530e718b688422727396", + "sha256:9f7c16705f44e0504a3a2a14197c1f0b32a95731d251777dcb060aa83022cb2d", + "sha256:9fb218c8a12e51d7ead2a7c9e101a04982237d4855716af2e9499306728fb485", + "sha256:a74ba0c356aaa3bb8e3eb79606a87669e7ec6444be352870623025d75a14a2bf", + "sha256:b4f69b3700201b80bb82c3a97d5e9254084f6dd5fb5b16fc1a7b974260f89f43", + "sha256:bc2ec7c7b5d66b8ec9ce9f720dbb5fa4bace0f545acd34870eff4a369b44bf37", + "sha256:c189af0545965fa8d3b9613cfdb0cd37f9d71349e0f7750e1fd704648d475ed2", + "sha256:c1fbe7621c167ecaa38ad29643d77a9ce7311583761abf7836e1510c580bf3dd", + "sha256:c7cf14a27b0d6adfaebb3ae4153f1e516df54e47e42dcc073d7b3d76111a8d86", + "sha256:c9f72a021fbb792ce98306ffb0c348b3c9cb967dce0f12a49aa4c3d3fdefa967", + "sha256:cd25d2a9d2b36fcb318882481367956d2cf91329f6892fe5d385c346c0649629", + "sha256:ce543ed15570eedbb85df19b0a1a7314a9c8141a36ce089c0a894adbfccb4568", + "sha256:ce7b031a6fc11365970e6a5686d7ba8c63e4c1cf1ea143811acbb524295eabed", + "sha256:d35e3c8d9b1268cbf5d3670285feb3528f6680420eafe35cccc686b73c1e330f", + "sha256:d50b6aec14bc737742ca96e85d6d0a5f9bfbded018264b3b70ff9d8c33485551", + "sha256:d5d0dae4cfd56969d23d94dc8e89fb6a217be461c69090768227beb8ed28c0a3", + "sha256:d5db32e2a6ccbb3d34d87c87b432959e0db29755727afb37290e10f6e8e62614", + "sha256:d72e2ecc68a942e8cf9739619b7f408cc7b272b279b56b2c83c6123fcfa5cdff", + "sha256:d737a602fbd82afd892ca746392401b634e278cb65d55c4b7a8f48e9ef8d008d", + "sha256:d80cf684b541685fccdd84c485b31ce73fc5c9b5d7523bf1394ce134a60c6883", + "sha256:db24668940f82321e746773a4bc617bfac06ec831e5c88b643f91f122a785684", + "sha256:dbc02381779d412145331789b40cc7b11fdf449e5d94f6bc0b080db0a56ea3f0", + "sha256:dffe31a7f47b603318c609f378ebcd57f1554a3a6a8effbc59c3c69f804296de", + "sha256:edf4392b77bdc81f36e92d3a07a5cd072f90253197f4a52a55a8cec48a12483b", + "sha256:efe8c0681042536e0d06c11f48cebe759707c9e9abf880ee213541c5b46c5bf3", + "sha256:f31f9fdbfecb042d046f9d91270a0ba28368a723302786c0009ee9b9f1f60199", + "sha256:f88a0b92277de8e3ca715a0d79d68dc82807457dae3ab8699c758f07c20b3c51", + "sha256:faaf07ea35355b01a35cb442dd950d8f1bb5b040a7787791a535de13db15ed90" + ], + "index": "pypi", + "version": "==10.0.0" + }, + "pyparsing": { + "hashes": [ + "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb", + "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc" + ], + "markers": "python_full_version >= '3.6.8'", + "version": "==3.0.9" + }, + "python-dateutil": { + "hashes": [ + "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", + "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.8.2" + }, + "requests": { + "hashes": [ + "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", + "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" + ], + "index": "pypi", + "version": "==2.31.0" + }, + "six": { + "hashes": [ + "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", + "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.16.0" + }, + "tqdm": { + "hashes": [ + "sha256:d302b3c5b53d47bce91fea46679d9c3c6508cf6332229aa1e7d8653723793386", + "sha256:d88e651f9db8d8551a62556d3cff9e3034274ca5d66e93197cf2490e2dcb69c7" + ], + "index": "pypi", + "version": "==4.66.1" + }, + "urllib3": { + "hashes": [ + "sha256:8d22f86aae8ef5e410d4f539fde9ce6b2113a001bb4d189e0aed70642d602b11", + "sha256:de7df1803967d2c2a98e4b11bb7d6bd9210474c46e8a0401514e3a42a75ebde4" + ], + "markers": "python_version >= '3.7'", + "version": "==2.0.4" + }, + "zipp": { + "hashes": [ + "sha256:679e51dd4403591b2d6838a48de3d283f3d188412a9782faadf845f298736ba0", + "sha256:ebc15946aa78bd63458992fc81ec3b6f7b1e92d51c35e6de1c3804e73b799147" + ], + "markers": "python_version < '3.10'", + "version": "==3.16.2" + } + }, + "develop": {} +} diff --git a/README.md b/README.md index ff24b4b..1a3ccd3 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,133 @@ -# XMind +# XMindCopilot -![mind_mapping](https://raw.githubusercontent.com/zhuifengshen/xmind/master/images/xmind.png) +**[XMindCopilot](https://github.com/MasterYip/XmindCopilot)** is an enhanced mindmap toolkit based on [zhuifengshen/xmind](https://github.com/zhuifengshen/xmind), providing advanced features for XMind file creation, editing, compression, format conversion, multi-file content search, and intelligent topic clustering. -**[XMind](https://github.com/zhuifengshen/xmind)** 是基于 Python 实现,提供了对 [XMind思维导图](https://www.xmind.cn/)进行创建、解析、更新的一站式解决方案! +## 🚀 New Features (Enhanced from Upstream) +This project extends the original [xmind-sdk-python](https://github.com/zhuifengshen/xmind) with the following advanced capabilities: -### 一、安装方式 +### 📱 Applications +- **🔍 Global Search** (`apps/global_search.py`): Batch search across multiple XMind files with colored output +- **📝 Markdown to XMind** (`apps/md2xmind.py`): Convert Markdown documents to XMind format with equation, image, and table support + +### 🛠️ Enhanced Core Modules + +#### 🗜️ File Compression (`XmindCopilot/file_shrink`) +- **Smart Image Compression**: PNG/JPEG optimization using pngquant and OpenCV +- **Batch Processing**: Compress entire directories of XMind files +- **Quality Control**: Configurable compression levels +- **Size Reduction**: Typical 60-80% file size reduction + +#### 🔍 Advanced Search (`XmindCopilot/search`) +- **Topic Search**: Find topics by title, hyperlink, or regex patterns +- **Batch Search**: Search across multiple XMind files simultaneously +- **Depth Control**: Limit search depth for performance +- **Highlighted Results**: Color-coded search results in terminal + +#### 🎯 Topic Clustering (`XmindCopilot/topic_cluster`) +- **Text Clustering**: Group similar topics using Jaccard similarity +- **Smart Segmentation**: Chinese/English text segmentation with jieba/spacy +- **Customizable Thresholds**: Adjustable similarity parameters +- **Visual Separation**: Automatic insertion of separator topics + +#### 🔄 Format Conversion (`XmindCopilot/fmt_cvt`) +- **Markdown ↔ XMind**: Bidirectional conversion with structure preservation +- **LaTeX Rendering**: Mathematical equations to images +- **Table Rendering**: Markdown tables to PNG with formatting support +- **Web Image Support**: Automatic image downloading and embedding + +#### 🎮 Project Management (`XmindCopilot/playerone_mgr`) +- **Topic Transfer**: Move topics between XMind files +- **Link Management**: Update hyperlinks automatically +- **Batch Operations**: Process multiple files efficiently + +## 🐛 Known Issues + +- **IMPORTANT**: Unzipping XMind files may cause storage leaks +- Special characters in XMind files can cause loading errors +```txt +Characters: 、、、 ``` -pip3 install XMind -or +## 📖 Usage Guide -pip3 install xmind +### 🆕 New Features Usage + +#### Global Search Application +```python +from XmindCopilot.search import BatchSearch +import glob + +# Get XMind file paths +xmind_paths = glob.glob('**/*.xmind', recursive=True) + +# Perform batch search +results = BatchSearch("search_term", xmind_paths, verbose=True) ``` +#### Markdown to XMind Conversion +```python +from XmindCopilot.fmt_cvt.md2xmind import MarkDown2Xmind +import XmindCopilot + +# Load existing XMind or create new +workbook = XmindCopilot.load("target.xmind") +root_topic = workbook.getPrimarySheet().getRootTopic() -### 二、版本升级 +# Convert Markdown content +md2xmind = MarkDown2Xmind(root_topic) +with open("document.md", "r", encoding="utf-8") as f: + md_content = f.read() + +md2xmind.convert2xmind(md_content, + cvtEquation=True, # Convert LaTeX equations + cvtWebImage=True, # Download web images + cvtHyperLink=True, # Process hyperlinks + cvtTable=True) # Render tables + +XmindCopilot.save(workbook) ``` -pip3 install -U XMind + +#### File Compression +```python +from XmindCopilot.file_shrink import xmind_shrink + +# Compress single file or directory +xmind_shrink("path/to/file.xmind", + PNG_Quality=10, # pngquant: 1-100 (low-high) + JPEG_Quality=20, # OpenCV: 0-100 (low-high) + use_pngquant=True, # Use pngquant for better PNG compression + replace=True) # Replace original file +``` + +#### Advanced Topic Search +```python +from XmindCopilot.search import topic_search, BatchSearch + +# Search within a topic hierarchy +target_topic = topic_search(root_topic, "search_term", depth=2) + +# Search with regex +regex_topic = topic_search(root_topic, r"^\d+\.", re_match=True) +``` + +#### Topic Clustering +```python +from XmindCopilot.topic_cluster import topic_cluster, ClusterArgs + +# Configure clustering parameters +args = ClusterArgs() +args.threshold = 0.3 # Similarity threshold +args.sample_number = 5 # Samples per cluster + +# Cluster topics automatically +topic_cluster(target_topic, recursive=True, args=args) ``` +### 📚 Basic Usage (Upstream Features) -### 三、使用方式 +#### 1. Creating XMind Files -#### 1、创建XMind文件 ``` def gen_my_xmind_file(): # 1、如果指定的XMind文件存在,则加载,否则创建一个新的 @@ -143,12 +247,13 @@ def gen_sheet2(workbook, sheet1): # 添加一个主题与主题之间的联系 sheet2.createRelationship(topic1.getID(), topic2.getID(), "relationship test") ``` -具体代码参考:[create_xmind.py](https://github.com/zhuifengshen/xmind/blob/master/example/create_xmind.py) +具体代码参考:[create_xmind.py](https://github.com/zhuifengshen/xmind/blob/master/example/create_xmind.py) #### 2、解析XMind文件 ##### (1) 将XMind文件转换为Dict数据 / JSON数据 + ``` import xmind workbook = xmind.load('demo.xmind') @@ -320,6 +425,7 @@ Output: ``` ##### (2)将画布转换为Dict数据 + ``` import xmind workbook = xmind.load('demo.xmind') @@ -394,6 +500,7 @@ Output: ``` ##### (3) 将主题转换为Dict数据 + ``` import xmind workbook = xmind.load('demo.xmind') @@ -465,6 +572,7 @@ Output: ``` ##### (4) 自定义解析 + ``` import xmind workbook = xmind.load('demo.xmind') @@ -501,27 +609,28 @@ def custom_parse_xmind(workbook): Output: Sheet : 'first sheet' - RootTopic : 'root node' - AttachedSubTopic : 'first sub topic' - AttachedSubTopic : 'second sub topic' - AttachedSubTopic : 'third sub topic' - AttachedSubTopic : 'fourth sub topic' - DetachedSubtopic : 'detached topic' + RootTopic : 'root node' + AttachedSubTopic : 'first sub topic' + AttachedSubTopic : 'second sub topic' + AttachedSubTopic : 'third sub topic' + AttachedSubTopic : 'fourth sub topic' + DetachedSubtopic : 'detached topic' Sheet : 'second sheet' - RootTopic : 'root node' - AttachedSubTopic : 'redirection to the first sheet' - AttachedSubTopic : 'topic with an url hyperlink' - AttachedSubTopic : 'topic with + RootTopic : 'root node' + AttachedSubTopic : 'redirection to the first sheet' + AttachedSubTopic : 'topic with an url hyperlink' + AttachedSubTopic : 'topic with notes' - AttachedSubTopic : 'topic with a file' + AttachedSubTopic : 'topic with a file' Relationship: [redirection to the first sheet] --> [topic with an url hyperlink] ``` -具体代码参考:[parse_xmind.py](https://github.com/zhuifengshen/xmind/blob/master/example/parse_xmind.py) +具体代码参考:[parse_xmind.py](https://github.com/zhuifengshen/xmind/blob/master/example/parse_xmind.py) #### 3、更新保存XMind文件 ##### (1)五种保存方法 + ``` import xmind # 加载XMind文件demo.xmind @@ -546,102 +655,77 @@ xmind.save(workbook=workbook, path='xmind_update_demo3.xmind', except_revisions= # 5、不指定保存路径,直接更新原文件 xmind.save(workbook) ``` -具体代码参考:[update_xmind.py](https://github.com/zhuifengshen/xmind/blob/master/example/update_xmind.py) +具体代码参考:[update_xmind.py](https://github.com/zhuifengshen/xmind/blob/master/example/update_xmind.py) ##### (2)XMind文件结构 ![xmind file structure](https://raw.githubusercontent.com/zhuifengshen/xmind/master/images/xmind_file_structure.png) +## 🔧 Supported Features -### 四、工具支持功能 - -#### 1、支持XMind以下原生元素的创建、解析和更新 -- 画布(Sheet) -- 主题(Topic:固定主题、自由主题) -- 图标(Marker:[图标名称](https://github.com/zhuifengshen/xmind/blob/master/xmind/core/markerref.py)) -- 备注(Note) -- 标签(Label) -- 批注(Comment) -- 联系(Relationship) -- 样式(Styles) - -#### 2、XMind原生元素 - -![xmind_native_elements](https://raw.githubusercontent.com/zhuifengshen/xmind/master/images/xmind_native_elements.png) - -其中,暂不支持的元素(日常也比较少用到) -- 标注(cllout topic) -- 概要(summary topic) -- 外框(outline border) -- 附件 - - -### 五、应用场景 - -[XMind2TestCase](https://github.com/zhuifengshen/xmind2testcase):一个高效测试用例设计的解决方案! - -该方案通过制定测试用例通用模板, 然后使用 XMind 这款广为流传且开源的思维导图工具进行用例设计。 - -然后基于通用的测试用例模板,在 XMind 文件上解析并提取出测试用例所需的基本信息, 合成常见测试用例管理系统所需的用例导入文件。 +### Core XMind Elements (Upstream) +- Sheets, Topics (attached/detached), Markers, Notes, Labels, Comments, Relationships, Styles -实现将 XMind 设计测试用例的便利与常见测试用例系统的高效管理完美结合起来了,提升日常测试工作的效率! +### Enhanced Features (This Fork) +- **File Compression**: PNG/JPEG optimization with configurable quality +- **Advanced Search**: Multi-file, regex, depth-limited search +- **Format Conversion**: MD↔XMind, LaTeX rendering, table generation +- **Topic Clustering**: Intelligent grouping with text similarity +- **Batch Operations**: Process multiple files efficiently +- **Project Management**: Topic transfer between files -使用流程如下: +## 📋 Applications -#### 1、使用Web工具进行XMind用例文件解析 +### Test Case Management (Upstream) +[XMind2TestCase](https://github.com/zhuifengshen/xmind2testcase) - Convert XMind files to test case formats for TestLink, Zentao, etc. -![webtool](https://raw.githubusercontent.com/zhuifengshen/xmind/master/images/webtool.png) +### Research & Documentation (New) +- **Academic Writing**: Convert papers/notes between Markdown and XMind +- **Knowledge Management**: Cluster and organize large topic collections +- **File Optimization**: Reduce XMind file sizes for sharing/storage +- **Multi-project Search**: Find information across project repositories -#### 2、转换后的用例预览 +## 🧪 Testing -![testcase preview](https://raw.githubusercontent.com/zhuifengshen/xmind/master/images/testcase_preview.png) - -#### 3、用例导入TestLink系统 - -![testlink](https://raw.githubusercontent.com/zhuifengshen/xmind/master/images/testlink.png) - -#### 4、用例导入Zentao(禅道)系统 - -![zentao](https://raw.githubusercontent.com/zhuifengshen/xmind/master/images/zentao.png) - - -### 六、自动化测试与发布 - -#### 1、自动化单元测试(TODO: 待上传) +Run the test suite: +```bash +python -m unittest discover ``` -python3 -m unittest discover -``` - -#### 2、一键打 Tag 并上传至 PYPI -每次在 __ about __.py 更新版本号后,运行以下命令,实现自动化更新打包上传至 [PYPI](https://pypi.org/) ,同时根据其版本号自动打 Tag 并推送到仓库: -``` -python3 setup.py pypi -``` -![upload pypi](https://raw.githubusercontent.com/zhuifengshen/xmind/master/images/pypi_upload.png) +Individual feature tests: +```bash +# Test file compression +python -m unittest test.XmindCopilot_test.TestXmindShrink +# Test search functionality +python -m unittest test.XmindCopilot_test.TestSearch +# Test format conversion +python -m unittest test.XmindCopilot_test.TestXmindFmtConvert +``` +## 🙏 Acknowledgments -### 七、致谢 -在此,衷心感谢 **XMind 思维导图**官方创造了这么一款激发灵感、创意,提升工作、生活效率的高价值生产力产品, -同时还开源 [xmind-sdk-python](https://github.com/xmindltd/xmind-sdk-python) 工具帮助开发者构建自己的 XMind 文件 ,本项目正是基于此工具进行扩展和升级,受益匪浅,感恩! - -得益于开源,也将坚持开源,并为开源贡献自己的点滴之力。后续,将继续根据实际项目需要,定期进行维护更新和完善,欢迎大伙的使用和[意见反馈](https://github.com/zhuifengshen/xmind/issues/new),谢谢! - -(如果本项目对你有帮助的话,也欢迎 _**[star](https://github.com/zhuifengshen/xmind)**_ ) +This project is built upon the excellent work of: +- **[zhuifengshen/xmind](https://github.com/zhuifengshen/xmind)** - The original enhanced XMind SDK +- **[XMind Official](https://xmind.net)** - For creating the XMind mindmapping software +- **[xmind-sdk-python](https://github.com/xmindltd/xmind-sdk-python)** - The foundation XMind SDK -![QA之禅](http://upload-images.jianshu.io/upload_images/139581-27c6030ba720846f.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +Special thanks to the open source community for the underlying libraries: +- pngquant for PNG compression +- jieba for Chinese text segmentation +- matplotlib for rendering capabilities +## 📄 License -### LICENSE ``` The MIT License (MIT) Copyright (c) 2019 Devin https://zhangchuzhao.site Copyright (c) 2013 XMind, Ltd +Copyright (c) 2023 MasterYip (Enhanced Features) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/README_legacy.md b/README_legacy.md new file mode 100644 index 0000000..530d091 --- /dev/null +++ b/README_legacy.md @@ -0,0 +1,657 @@ +# XMindCopilot + +**[XMindCopilot](https://github.com/MasterYip/XmindCopilot)**是一款思维导图辅助工具,支持xmind创建、编辑、压缩、格式互转、多文件内容检索等功能。A mindmap tool that provides assistance in creating, editing, compressing, format conversion, and multi-file content search for XMind files. + +### Bugs + +- **IMPORTANT** unzip xmind file to get reference_dir will lead to storage leak +- Special characters in xmind file will lead to error when loading xmind file + +```txt +SFTR Global search 无法读取符号: 、、、 +``` + +### 三、使用方式 + +#### 1、创建XMind文件 + +``` +def gen_my_xmind_file(): + # 1、如果指定的XMind文件存在,则加载,否则创建一个新的 + workbook = xmind.load("my.xmind") + + # 2、获取第一个画布(Sheet),默认新建一个XMind文件时,自动创建一个空白的画布 + sheet1 = workbook.getPrimarySheet() + # 对第一个画布进行设计完善,具体参照下一个函数 + design_sheet1(sheet1) + + # 3、创建第二个画布 + gen_sheet2(workbook, sheet1) + + # 4、保存(如果指定path参数,另存为该文件名) + xmind.save(workbook, path='test.xmind') +``` + +![first sheet](https://raw.githubusercontent.com/zhuifengshen/xmind/master/images/first_sheet.png) + +``` +def design_sheet1(sheet1): + # ***** 第一个画布 ***** + sheet1.setTitle("first sheet") # 设置画布名称 + + # 获取画布的中心主题,默认创建画布时会新建一个空白中心主题 + root_topic1 = sheet1.getRootTopic() + root_topic1.setTitle("root node") # 设置主题名称 + + # 创建一个子主题,并设置其名称 + sub_topic1 = root_topic1.addSubTopic() + sub_topic1.setTitle("first sub topic") + + sub_topic2 = root_topic1.addSubTopic() + sub_topic2.setTitle("second sub topic") + + sub_topic3 = root_topic1.addSubTopic() + sub_topic3.setTitle("third sub topic") + + sub_topic4 = root_topic1.addSubTopic() + sub_topic4.setTitle("fourth sub topic") + + # 除了新建子主题,还可以创建自由主题(注意:只有中心主题支持创建自由主题) + detached_topic1 = root_topic1.addSubTopic(topics_type=TOPIC_DETACHED) + detached_topic1.setTitle("detached topic") + detached_topic1.setPosition(0, 30) + + # 创建一个子主题的子主题 + sub_topic1_1 = sub_topic1.addSubTopic() + sub_topic1_1.setTitle("I'm a sub topic too") +``` + +![second sheet](https://raw.githubusercontent.com/zhuifengshen/xmind/master/images/second_sheet.png) + +``` +def gen_sheet2(workbook, sheet1): + # ***** 设计第二个画布 ***** + sheet2 = workbook.createSheet() + sheet2.setTitle("second sheet") + + # 获取画布的中心主题 + root_topic2 = sheet2.getRootTopic() + root_topic2.setTitle("root node") + + # 使用另外一种方法创建子主题 + topic1 = TopicElement(ownerWorkbook=workbook) + # 给子主题添加一个主题间超链接,通过指定目标主题ID即可,这里链接到第一个画布 + topic1.setTopicHyperlink(sheet1.getID()) + topic1.setTitle("redirection to the first sheet") + + topic2 = TopicElement(ownerWorkbook=workbook) + topic2.setTitle("topic with an url hyperlink") + # 给子主题添加一个URL超链接 + topic2.setURLHyperlink("https://github.com/zhuifengshen/xmind") + + topic3 = TopicElement(ownerWorkbook=workbook) + topic3.setTitle("third node") + # 给子主题添加一个备注(快捷键F4) + topic3.setPlainNotes("notes for this topic") + topic3.setTitle("topic with \n notes") + + topic4 = TopicElement(ownerWorkbook=workbook) + # 给子主题添加一个文件超链接 + topic4.setFileHyperlink("logo.png") + topic4.setTitle("topic with a file") + + topic1_1 = TopicElement(ownerWorkbook=workbook) + topic1_1.setTitle("sub topic") + # 给子主题添加一个标签(目前XMind软件仅支持添加一个,快捷键) + topic1_1.addLabel("a label") + + topic1_1_1 = TopicElement(ownerWorkbook=workbook) + topic1_1_1.setTitle("topic can add multiple markers") + # 给子主题添加两个图标 + topic1_1_1.addMarker(MarkerId.starBlue) + topic1_1_1.addMarker(MarkerId.flagGreen) + + topic2_1 = TopicElement(ownerWorkbook=workbook) + topic2_1.setTitle("topic can add multiple comments") + # 给子主题添加一个批注(评论) + topic2_1.addComment("I'm a comment!") + topic2_1.addComment(content="Hello comment!", author='devin') + + # 将创建好的子主题添加到其父主题下 + root_topic2.addSubTopic(topic1) + root_topic2.addSubTopic(topic2) + root_topic2.addSubTopic(topic3) + root_topic2.addSubTopic(topic4) + topic1.addSubTopic(topic1_1) + topic2.addSubTopic(topic2_1) + topic1_1.addSubTopic(topic1_1_1) + + # 给中心主题下的每个子主题添加一个优先级图标 + topics = root_topic2.getSubTopics() + for index, topic in enumerate(topics): + topic.addMarker("priority-" + str(index + 1)) + + # 添加一个主题与主题之间的联系 + sheet2.createRelationship(topic1.getID(), topic2.getID(), "relationship test") +``` + +具体代码参考:[create_xmind.py](https://github.com/zhuifengshen/xmind/blob/master/example/create_xmind.py) + +#### 2、解析XMind文件 + +##### (1) 将XMind文件转换为Dict数据 / JSON数据 + +``` +import xmind +workbook = xmind.load('demo.xmind') +print(workbook.getData()) +print(workbook.to_prettify_json()) + + +Output: + +[ # 画布列表 + { # 第1个画布数据 + "id": "2cc3b068922063a81a20029655", # 画布ID + "title": "first sheet", # 画布名称 + "topic": { # 中心主题 + "id": "2cc3b06892206f95288e487b6c", # 主题ID + "link": null, # 超链接信息 + "title": "root node", # 主题名称 + "note": null, # 备注信息 + "label": null, # 便签信息 + "comment": null, # 批注(评论)信息 + "markers": [], # 图标列表 + "topics": [ # 子主题列表 + { + "id": "2cc3b06892206c816e1cb55ddc", # 子主题ID + "link": null, + "title": "first sub topic", + "note": null, + "label": null, + "comment": null, + "markers": [], + "topics": [ # 子主题下的子主题列表 + { + "id": "b0ed74214dbca939935b981906", + "link": null, + "title": "I'm a sub topic too", + "note": null, + "label": null, + "comment": null, + "markers": [] + } + ] + }, + { + "id": "b0ed74214dbca693b947ef03fa", + "link": null, + "title": "second sub topic", + "note": null, + "label": null, + "comment": null, + "markers": [] + }, + { + "id": "b0ed74214dbca1fe9ade911b94", + "link": null, + "title": "third sub topic", + "note": null, + "label": null, + "comment": null, + "markers": [] + }, + { + "id": "b0ed74214dbcac00c0eb368b53", + "link": null, + "title": "fourth sub topic", + "note": null, + "label": null, + "comment": null, + "markers": [] + } + ] + } + }, + { + "id": "b0ed74214dbcafdd0799f81ebf", + "title": "second sheet", # 第2个画布数据 + "topic": { + "id": "b0ed74214dbcac7567f88365c2", + "link": null, + "title": "root node", + "note": null, + "label": null, + "comment": null, + "markers": [], + "topics": [ + { + "id": "b0ed74214dbca8bfdc2b60df47", + "link": "xmind:#2cc3b068922063a81a20029655", + "title": "redirection to the first sheet", + "note": null, + "label": null, + "comment": null, + "markers": [ + "priority-1" + ], + "topics": [ + { + "id": "e613d79938591579e707a7a161", + "link": null, + "title": "sub topic", + "note": null, + "label": "a label", + "comment": null, + "markers": [], + "topics": [ + { + "id": "e613d799385912cca5eb579fb3", + "link": null, + "title": "topic can add multiple markers", + "note": null, + "label": null, + "comment": null, + "markers": [ + "star-blue", + "flag-green" + ] + } + ] + } + ] + }, + { + "id": "e613d79938591ef98b64a768db", + "link": "https://xmind.net", + "title": "topic with an url hyperlink", + "note": null, + "label": null, + "comment": null, + "markers": [ + "priority-2" + ], + "topics": [ + { + "id": "e613d799385916ed8f3ea382ca", + "link": null, + "title": "topic can add multiple comments", + "note": null, + "label": null, + "comment": "I'm a comment!\nHello comment!", + "markers": [] + } + ] + }, + { + "id": "e613d799385919451116404d66", + "link": null, + "title": "topic with \n notes", + "note": "notes for this topic", + "label": null, + "comment": null, + "markers": [ + "priority-3" + ] + }, + { + "id": "e613d7993859156671fa2c12a5", + "link": "file:///Users/zhangchuzhao/Project/python/tmp/xmind/example/xminddemo/logo.png", + "title": "topic with a file", + "note": null, + "label": null, + "comment": null, + "markers": [ + "priority-4" + ] + } + ] + } + } +] +``` + +##### (2)将画布转换为Dict数据 + +``` +import xmind +workbook = xmind.load('demo.xmind') +sheet = workbook.getPrimarySheet() +print(sheet.getData()) + + +Output: + +{ + "id": "2cc3b068922063a81a20029655", + "title": "first sheet", + "topic": { + "id": "2cc3b06892206f95288e487b6c", + "link": null, + "title": "root node", + "note": null, + "label": null, + "comment": null, + "markers": [], + "topics": [ + { + "id": "2cc3b06892206c816e1cb55ddc", + "link": null, + "title": "first sub topic", + "note": null, + "label": null, + "comment": null, + "markers": [], + "topics": [ + { + "id": "b0ed74214dbca939935b981906", + "link": null, + "title": "I'm a sub topic too", + "note": null, + "label": null, + "comment": null, + "markers": [] + } + ] + }, + { + "id": "b0ed74214dbca693b947ef03fa", + "link": null, + "title": "second sub topic", + "note": null, + "label": null, + "comment": null, + "markers": [] + }, + { + "id": "b0ed74214dbca1fe9ade911b94", + "link": null, + "title": "third sub topic", + "note": null, + "label": null, + "comment": null, + "markers": [] + }, + { + "id": "b0ed74214dbcac00c0eb368b53", + "link": null, + "title": "fourth sub topic", + "note": null, + "label": null, + "comment": null, + "markers": [] + } + ] + } +} +``` + +##### (3) 将主题转换为Dict数据 + +``` +import xmind +workbook = xmind.load('demo.xmind') +sheet = workbook.getPrimarySheet() +root_topic = sheet.getRootTopic() +print(root_topic.getData()) + + +Output: + +{ + "id": "2cc3b06892206f95288e487b6c", + "link": null, + "title": "root node", + "note": null, + "label": null, + "comment": null, + "markers": [], + "topics": [ + { + "id": "2cc3b06892206c816e1cb55ddc", + "link": null, + "title": "first sub topic", + "note": null, + "label": null, + "comment": null, + "markers": [], + "topics": [ + { + "id": "b0ed74214dbca939935b981906", + "link": null, + "title": "I'm a sub topic too", + "note": null, + "label": null, + "comment": null, + "markers": [] + } + ] + }, + { + "id": "b0ed74214dbca693b947ef03fa", + "link": null, + "title": "second sub topic", + "note": null, + "label": null, + "comment": null, + "markers": [] + }, + { + "id": "b0ed74214dbca1fe9ade911b94", + "link": null, + "title": "third sub topic", + "note": null, + "label": null, + "comment": null, + "markers": [] + }, + { + "id": "b0ed74214dbcac00c0eb368b53", + "link": null, + "title": "fourth sub topic", + "note": null, + "label": null, + "comment": null, + "markers": [] + } + ] +} +``` + +##### (4) 自定义解析 + +``` +import xmind +workbook = xmind.load('demo.xmind') +custom_parse_xmind(workbook) + + +def custom_parse_xmind(workbook): + elements = {} + + def _echo(tag, element, indent=0): + title = element.getTitle() + elements[element.getID()] = title + print('\t' * indent, tag, ':', pipes.quote(title)) + + def dump_sheet(sheet): + root_topic = sheet.getRootTopic() + _echo('RootTopic', root_topic, 1) + + for topic in root_topic.getSubTopics() or []: + _echo('AttachedSubTopic', topic, 2) + + for topic in root_topic.getSubTopics(xmind.core.const.TOPIC_DETACHED) or []: + _echo('DetachedSubtopic', topic, 2) + + for rel in sheet.getRelationships(): + id1, id2 = rel.getEnd1ID(), rel.getEnd2ID() + print('Relationship: [%s] --> [%s]' % (elements.get(id1), elements.get(id2))) + + for sheet in workbook.getSheets(): + _echo('Sheet', sheet) + dump_sheet(sheet) + + +Output: + + Sheet : 'first sheet' + RootTopic : 'root node' + AttachedSubTopic : 'first sub topic' + AttachedSubTopic : 'second sub topic' + AttachedSubTopic : 'third sub topic' + AttachedSubTopic : 'fourth sub topic' + DetachedSubtopic : 'detached topic' + Sheet : 'second sheet' + RootTopic : 'root node' + AttachedSubTopic : 'redirection to the first sheet' + AttachedSubTopic : 'topic with an url hyperlink' + AttachedSubTopic : 'topic with + notes' + AttachedSubTopic : 'topic with a file' +Relationship: [redirection to the first sheet] --> [topic with an url hyperlink] +``` + +具体代码参考:[parse_xmind.py](https://github.com/zhuifengshen/xmind/blob/master/example/parse_xmind.py) + +#### 3、更新保存XMind文件 + +##### (1)五种保存方法 + +``` +import xmind +# 加载XMind文件demo.xmind +workbook = xmind.load('demo.xmind') +primary_sheet = workbook.getPrimarySheet() +root_topic = primary_sheet.getRootTopic() +# 给中心主题添加一个星星图标 +root_topic.addMarker(MarkerId.starRed) + +# 第1种:默认保存所有的内容,这里保存时另存为xmind_update_demo.xmind(推荐) +xmind.save(workbook=workbook, path='xmind_update_demo.xmind') + +# 第2种:只保存思维导图内容content.xml核心文件,适用于没有添加评论、自定义样式和附件的情况 +xmind.save(workbook=workbook, path='xmind_update_demo1.xmind', only_content=True) + +# 第3种:只保存content.xml、comments.xml、styles.xml三个核心文件,适用于没有附件的情况 +xmind.save(workbook=workbook, path='xmind_update_demo2.xmind', except_attachments=True) + +# 4、除了修改记录,其他内容都保存,因为XMind文件的修改记录文件夹比较大,以便节约内存(推荐) +xmind.save(workbook=workbook, path='xmind_update_demo3.xmind', except_revisions=True) + +# 5、不指定保存路径,直接更新原文件 +xmind.save(workbook) +``` + +具体代码参考:[update_xmind.py](https://github.com/zhuifengshen/xmind/blob/master/example/update_xmind.py) + +##### (2)XMind文件结构 + +![xmind file structure](https://raw.githubusercontent.com/zhuifengshen/xmind/master/images/xmind_file_structure.png) + +### 四、工具支持功能 + +#### 1、支持XMind以下原生元素的创建、解析和更新 + +- 画布(Sheet) +- 主题(Topic:固定主题、自由主题) +- 图标(Marker:[图标名称](https://github.com/zhuifengshen/xmind/blob/master/xmind/core/markerref.py)) +- 备注(Note) +- 标签(Label) +- 批注(Comment) +- 联系(Relationship) +- 样式(Styles) + +#### 2、XMind原生元素 + +![xmind_native_elements](https://raw.githubusercontent.com/zhuifengshen/xmind/master/images/xmind_native_elements.png) + +其中,暂不支持的元素(日常也比较少用到) + +- 标注(cllout topic) +- 概要(summary topic) +- 外框(outline border) +- 附件 + +### 五、应用场景 + +[XMind2TestCase](https://github.com/zhuifengshen/xmind2testcase):一个高效测试用例设计的解决方案! + +该方案通过制定测试用例通用模板, 然后使用 XMind 这款广为流传且开源的思维导图工具进行用例设计。 + +然后基于通用的测试用例模板,在 XMind 文件上解析并提取出测试用例所需的基本信息, 合成常见测试用例管理系统所需的用例导入文件。 + +实现将 XMind 设计测试用例的便利与常见测试用例系统的高效管理完美结合起来了,提升日常测试工作的效率! + +使用流程如下: + +#### 1、使用Web工具进行XMind用例文件解析 + +![webtool](https://raw.githubusercontent.com/zhuifengshen/xmind/master/images/webtool.png) + +#### 2、转换后的用例预览 + +![testcase preview](https://raw.githubusercontent.com/zhuifengshen/xmind/master/images/testcase_preview.png) + +#### 3、用例导入TestLink系统 + +![testlink](https://raw.githubusercontent.com/zhuifengshen/xmind/master/images/testlink.png) + +#### 4、用例导入Zentao(禅道)系统 + +![zentao](https://raw.githubusercontent.com/zhuifengshen/xmind/master/images/zentao.png) + +### 六、自动化测试与发布 + +#### 1、自动化单元测试(TODO: 待上传) + +``` +python3 -m unittest discover +``` + +#### 2、一键打 Tag 并上传至 PYPI + +每次在 __about__.py 更新版本号后,运行以下命令,实现自动化更新打包上传至 [PYPI](https://pypi.org/) ,同时根据其版本号自动打 Tag 并推送到仓库: + +``` +python3 setup.py pypi +``` + +![upload pypi](https://raw.githubusercontent.com/zhuifengshen/xmind/master/images/pypi_upload.png) + +### 七、致谢 + +在此,衷心感谢 **XMind 思维导图**官方创造了这么一款激发灵感、创意,提升工作、生活效率的高价值生产力产品, +同时还开源 [xmind-sdk-python](https://github.com/xmindltd/xmind-sdk-python) 工具帮助开发者构建自己的 XMind 文件 ,本项目正是基于此工具进行扩展和升级,受益匪浅,感恩! + +得益于开源,也将坚持开源,并为开源贡献自己的点滴之力。后续,将继续根据实际项目需要,定期进行维护更新和完善,欢迎大伙的使用和[意见反馈](https://github.com/zhuifengshen/xmind/issues/new),谢谢! + +(如果本项目对你有帮助的话,也欢迎 _**[star](https://github.com/zhuifengshen/xmind)**_ ) + +![QA之禅](http://upload-images.jianshu.io/upload_images/139581-27c6030ba720846f.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +### LICENSE + +``` +The MIT License (MIT) + +Copyright (c) 2019 Devin https://zhangchuzhao.site +Copyright (c) 2013 XMind, Ltd + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +``` diff --git a/Test.py b/Test.py new file mode 100644 index 0000000..fca7d9b --- /dev/null +++ b/Test.py @@ -0,0 +1,50 @@ +import sys +if "../" not in sys.path: + sys.path.append("../") + +import re +from urllib.parse import unquote +from XmindCopilot.search import topic_search +from XmindCopilot.topic_cluster import topic_cluster +from XmindCopilot.file_shrink import xmind_shrink +from XmindCopilot.playerone_mgr import topic_info_transfer, topic_info_clear +import XmindCopilot + + +def topic_traverse(topic): + if topic.getHyperlink() and re.match("^file:(.*)\.xmind$", unquote(topic.getHyperlink())): + print(unquote(str(topic.getHyperlink()))) + topic.setHyperlink(unquote(topic.getHyperlink())+"8") + topics = topic.getSubTopics() + for t in topics: + topic_traverse(t) + + +# Topic info transfer (data transfer from player one to sub xmind) +xmind_path = "D:\\SFTR\\PlayerOS\\Player One.xmind8" +workbook = XmindCopilot.load(xmind_path) +rootTopic = workbook.getPrimarySheet().getRootTopic() +filetreeTopic = topic_search(rootTopic, "文件索引", 2) +# topic_info_transfer(filetreeTopic) +topic_info_clear(filetreeTopic) +XmindCopilot.save(workbook) + +# Topic Cluster (cluster topics in draft) +# xmind_path = "D:\\SFTR\\PlayerOS\\Player One.xmind8" +# workbook = XmindCopilot.load(xmind_path) +# rootTopic = workbook.getPrimarySheet().getRootTopic() +# draftTopic = topic_search(rootTopic, "Draft", 2) +# topic_cluster(draftTopic) +# XmindCopilot.save(workbook) + + +# HyperLink rename (from .xmind to .xmind8) +# xmind_path = "D:\\SFTR\\PlayerOS\\Player One.xmind8" +# workbook = XmindCopilot.load(xmind_path) +# rootTopic = workbook.getPrimarySheet().getRootTopic() +# topic_traverse(rootTopic) +# XmindCopilot.save(workbook) + +# Xmind Shrink +# xmind_path = "D:\\SFTR\\1 Course\\EE_Engineering\\5 电力电子技术" +# xmind_shrink(xmind_path) diff --git a/xmind/__about__.py b/XmindCopilot/__about__.py similarity index 73% rename from xmind/__about__.py rename to XmindCopilot/__about__.py index be1044e..ddad3f5 100644 --- a/xmind/__about__.py +++ b/XmindCopilot/__about__.py @@ -1,9 +1,9 @@ -__title__ = 'XMind' +__title__ = 'XMindCopilot' __description__ = 'XMind是基于Python实现,提供了对XMind思维导图进行创建、解析、更新的一站式解决方案!' __keywords__ = 'xmind, mind mapping, 思维导图, XMind思维导图', __url__ = 'https://github.com/zhuifengshen/xmind' -__author__ = 'Devin' -__author_email__ = '1324556701@qq.com' -__version__ = '1.2.0' +__author__ = 'MasterYip' +__author_email__ = '2205929492@qq.com' +__version__ = '0.0.1' __license__ = 'MIT' __cake__ = u'\u2728 \U0001f370 \u2728' diff --git a/xmind/__init__.py b/XmindCopilot/__init__.py similarity index 59% rename from xmind/__init__.py rename to XmindCopilot/__init__.py index d6626a8..ff8d1be 100644 --- a/xmind/__init__.py +++ b/XmindCopilot/__init__.py @@ -2,21 +2,21 @@ # -*- coding: utf-8 -*- """ - xmind + XmindCopilot """ __version__ = "0.1.0" -from xmind.core.loader import WorkbookLoader -from xmind.core.saver import WorkbookSaver +from .core.loader import WorkbookLoader +from .core.saver import WorkbookSaver -def load(path): +def load(path, get_refs=True): """ Load XMind workbook from given path. If file no exist on given path then created new one. """ loader = WorkbookLoader(path) - return loader.get_workbook() + return loader.get_workbook(get_refs) def save(workbook, path=None, only_content=False, except_attachments=False, except_revisions=False): """ Save workbook to given path. If path not given, then will save to path that set to workbook. """ saver = WorkbookSaver(workbook) - saver.save(path=path, only_content=only_content, except_attachments=except_attachments, except_revisions=except_revisions) - + saver.save(path=path, only_content=only_content, + except_attachments=except_attachments, except_revisions=except_revisions) diff --git a/xmind/core/__init__.py b/XmindCopilot/core/__init__.py similarity index 99% rename from xmind/core/__init__.py rename to XmindCopilot/core/__init__.py index 756bac9..7a4bfe5 100644 --- a/xmind/core/__init__.py +++ b/XmindCopilot/core/__init__.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- """ -xmind.core +XmindCopilot.core """ from xml.dom import minidom as DOM from .. import utils @@ -138,6 +138,7 @@ def iterChildNodesByTagName(self, tag_name): if node.tagName == tag_name: yield node + # Remove child node def removeChild(self, child_node): child_node = child_node.getImplementation() self._node.removeChild(child_node) @@ -309,3 +310,6 @@ def setTextContent(self, data): self._node.appendChild(text) + def getnode(self): + return self._node + diff --git a/xmind/core/comments.py b/XmindCopilot/core/comments.py similarity index 97% rename from xmind/core/comments.py rename to XmindCopilot/core/comments.py index c6456b6..d63aa3d 100644 --- a/xmind/core/comments.py +++ b/XmindCopilot/core/comments.py @@ -2,12 +2,12 @@ # _*_ coding:utf-8 _*_ """ -xmind.core.comments implements encapsulation of the XMind comments.xml. +XmindCopilot.core.comments implements encapsulation of the XMind comments.xml. """ import random -from xmind import utils -from xmind.core import Document, const, Element +from .. import utils +from . import Document, const, Element class CommentsBookDocument(Document): diff --git a/xmind/core/const.py b/XmindCopilot/core/const.py similarity index 85% rename from xmind/core/const.py rename to XmindCopilot/core/const.py index 87865fe..56fe25f 100644 --- a/xmind/core/const.py +++ b/XmindCopilot/core/const.py @@ -6,12 +6,14 @@ """ XMIND_EXT = ".xmind" +XMIND8_EXT = ".xmind8" VERSION = "2.0" # Namespace NAMESPACE = "xmlns" XMLNS_CONTENT = "urn:xmind:xmap:xmlns:content:2.0" XMLNS_COMMENTS = "urn:xmind:xmap:xmlns:comments:2.0" XMLNS_STYLE = "urn:xmind:xmap:xmlns:style:2.0" +XMLNS_MANIFEST = "urn:xmind:xmap:xmlns:manifest:2.0" NS_URI = "http://www.w3.org/1999/xhtml" NS_FO = (NS_URI, "fo", "http://www.w3.org/1999/XSL/Format") NS_SVG = (NS_URI, "svg", "http://www.w3.org/2000/svg") @@ -39,6 +41,7 @@ TAG_TITLE = "title" TAG_POSITION = "position" TAG_CHILDREN = "children" +TAG_IMAGE = "xhtml:img" TAG_NOTES = "notes" TAG_LABEL = "label" TAG_LABELS = "labels" @@ -53,6 +56,8 @@ TAG_COMMENTSBOOK = "comments" TAG_COMMENT = "comment" TAG_CONTENT = "content" +TAG_MANIFESTBOOK = "manifest" +TAG_FILE_ENTRY = "file-entry" # Attr ATTR_VERSION = "version" ATTR_ID = "id" @@ -74,6 +79,16 @@ ATTR_AUTHOR = "author" ATTR_OBJECT_ID = "object-id" ATTR_TIME = "time" +# Attr title +ATTR_TITLE_SVGWIDTH = "svg:width" +# Attr xhtml:img +ATTR_IMG_ALIGN = "align" +ATTR_IMG_HEIGHT = "svg:height" +ATTR_IMG_WIDTH = "svg:width" +ATTR_IMG_SRC = "xhtml:src" +# Attr Manifest file-entry +ATTR_FULL_PATH = "full-path" +ATTR_MEDIA_TYPE = "media-type" # Topic Type VAL_FOLDED = "folded" TOPIC_ROOT = "root" diff --git a/XmindCopilot/core/image.py b/XmindCopilot/core/image.py new file mode 100644 index 0000000..f1acb78 --- /dev/null +++ b/XmindCopilot/core/image.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" + XmindCopilot.core.image + +""" +from . import const +from .mixin import WorkbookMixinElement +from . import utils +import os +import re +import shutil +from PIL.Image import Image +from typing import Optional, Union + +class ImageElement(WorkbookMixinElement): + TAG_NAME = const.TAG_IMAGE + + def __init__(self, node=None, ownerWorkbook=None): + super(ImageElement, self).__init__(node, ownerWorkbook) + + def _getImgAbsPath(self): + src = self.getAttribute(const.ATTR_IMG_SRC) + refdir = self.getOwnerWorkbook().reference_dir + if src and refdir: + return os.path.join(refdir, src.split(":")[1]) + + def _getImgAttribute(self): + """ + Get image attributes + + :return: (src, align, height, width) + """ + align = self.getAttribute(const.ATTR_IMG_ALIGN) + height = self.getAttribute(const.ATTR_IMG_HEIGHT) + width = self.getAttribute(const.ATTR_IMG_WIDTH) + src = self.getAttribute(const.ATTR_IMG_SRC) + return (src, align, height, width) + + def _setImgAttribute(self, src=None, align=None, height=None, width=None): + """ + Set image attributes. + + :param src: image source (xap:attachments/). If src is not None, it WON'T be changed. + :param align: image align (["top", "bottom", "left", "right"]). if it is None, it will be removed(Defaults to aligning top). + :param height: image svg:height. If it is None, it will be removed. + :param width: image svg:width. If it is None, it will be removed. + """ + if src is not None: + self.setAttribute(const.ATTR_IMG_SRC, src) + if align in ["top", "bottom", "left", "right", None]: + self.setAttribute(const.ATTR_IMG_ALIGN, align) + self.setAttribute(const.ATTR_IMG_HEIGHT, height) + self.setAttribute(const.ATTR_IMG_WIDTH, width) + + def _setImageFile(self, img: Union[Image, str]): + """ + Set image file + + :param img: image path or Image obj to set. + """ + + # Delete origin image file + if self._getImgAbsPath() and os.path.isfile(self._getImgAbsPath()): + os.remove(self._getImgAbsPath()) + + # Handle Web img + if type(img) is str and re.match("^http[s]{0,1}://.*$", img): + self.setAttribute(const.ATTR_IMG_SRC, img) + return + + # Set image file + attach_dir = self.getOwnerWorkbook().get_attachments_path() + if type(img) == str: + ext_name = os.path.splitext(img)[1] + else: + ext_name = ".png" + media_type = "image/"+ext_name[1:] + img_name = utils.generate_id()+ext_name + save_path = os.path.join(attach_dir, img_name) + # Copy image file + if type(img) == str: + shutil.copy(img, save_path) + else: + img.save(save_path, format='png') + + # Set xhtml:src Attr + attr_src = "xap:attachments/"+img_name + self.setAttribute(const.ATTR_IMG_SRC, attr_src) + self.getOwnerWorkbook().manifestbook.addManifest("attachments/"+img_name, media_type) + + + + + + def setImage(self, img: Optional[Union[Image, str]] = None, + align=None, height=None, width=None): + """ + Set the image and its attr + + :param img: image path or Image obj to set. If img is None, original img will be reserved. + :param align: image align (["top", "bottom", "left", "right"]). if it is None, it will be removed(Defaults to aligning top). + :param height: image svg:height. If it is None, it will be removed. + :param width: image svg:width. If it is None, it will be removed. + """ + if img: + self._setImageFile(img) + self._setImgAttribute(align=align, height=height, width=width) diff --git a/xmind/core/labels.py b/XmindCopilot/core/labels.py similarity index 96% rename from xmind/core/labels.py rename to XmindCopilot/core/labels.py index b3f0b8d..9c155b8 100644 --- a/xmind/core/labels.py +++ b/XmindCopilot/core/labels.py @@ -3,7 +3,7 @@ """ - xmind.core.labels + XmindCopilot.core.labels """ from . import const from .mixin import TopicMixinElement diff --git a/XmindCopilot/core/loader.py b/XmindCopilot/core/loader.py new file mode 100644 index 0000000..5f37c90 --- /dev/null +++ b/XmindCopilot/core/loader.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +XmindCopilot.core.loader +""" +from .comments import CommentsBookDocument +from .styles import StylesBookDocument +from .manifest import ManifestBookDocument +from . import const +from .workbook import WorkbookDocument +from .. import utils +import os + + +class WorkbookLoader(object): + def __init__(self, path): + """ Load XMind workbook from given path + + :param path: path to XMind file. If not an existing file, will not raise an exception. + + """ + super(WorkbookLoader, self).__init__() + self._input_source = utils.get_abs_path(path) + + file_name, ext = utils.split_ext(self._input_source) + + if ext != const.XMIND_EXT and ext != const.XMIND8_EXT: + raise Exception( + "The XMind filename is missing the '%s' extension!" % const.XMIND_EXT) + + # Input Stream + self._content_stream = None + self._styles_stream = None + self._comments_stream = None + self._manifest_stream = None + + try: + with utils.extract(self._input_source) as input_stream: + for stream in input_stream.namelist(): + if stream == const.CONTENT_XML: + self._content_stream = utils.parse_dom_string( + input_stream.read(stream)) + elif stream == const.STYLES_XML: + self._styles_stream = utils.parse_dom_string( + input_stream.read(stream)) + elif stream == const.COMMENTS_XML: + self._comments_stream = utils.parse_dom_string( + input_stream.read(stream)) + elif stream == const.MANIFEST_XML: + self._manifest_stream = utils.parse_dom_string( + input_stream.read(stream)) + + except BaseException: + # FIXME: illegal char in xmind & illegal file name should be distinguished + pass + + def get_workbook(self, get_refs=True): + """ Parse XMind file to `WorkbookDocument` object and return + """ + path = self._input_source + content = self._content_stream + styles = self._styles_stream + comments = self._comments_stream + manifest = self._manifest_stream + stylesbook = StylesBookDocument(node=styles, path=path) + commentsbook = CommentsBookDocument(node=comments, path=path) + manifestbook = ManifestBookDocument(node=manifest, path=path) + reference_dir = None + if get_refs: + reference_dir = self.get_reference() + workbook = WorkbookDocument(node=content, path=path, + stylesbook=stylesbook, commentsbook=commentsbook, + manifestbook=manifestbook, reference_dir=reference_dir) + + return workbook + + def get_stylesbook(self): + """ Parse Xmind styles.xml to `StylesBookDocument` object and return + """ + content = self._styles_stream + path = self._input_source + + stylesbook = StylesBookDocument(node=content, path=path) + return stylesbook + + def get_commentsbook(self): + content = self._comments_stream + path = self._input_source + + commentsbook = CommentsBookDocument(node=content, path=path) + return commentsbook + + def get_manifestbook(self): + content = self._manifest_stream + path = self._input_source + + manifestbook = ManifestBookDocument(node=content, path=path) + return manifestbook + + def get_reference(self, except_revisions=False): + """ + Get all references(image, etc.) in xmind zip file. + + :param except_revisions: whether or not to save `Revisions` content in order ot save space. + :return: the temp reference directory path + """ + original_xmind_file = self._input_source + # FIXME: This may take huge space, so we should delete it after use.(with statement) + reference_dir = utils.temp_dir() + if os.path.isfile(original_xmind_file): + filename, suffix = utils.split_ext(original_xmind_file) + if suffix != const.XMIND_EXT and suffix != const.XMIND8_EXT: + raise Exception( + 'XMind filename require a "%s" extension' % const.XMIND_EXT) + + original_zip = utils.extract(original_xmind_file) + try: + with original_zip as input_stream: + for name in input_stream.namelist(): + if name in [const.CONTENT_XML, const.STYLES_XML, const.COMMENTS_XML, const.MANIFEST_XML]: + continue + if const.REVISIONS_DIR in name and except_revisions: + continue + target_file = utils.get_abs_path( + utils.join_path(reference_dir, name)) + if not os.path.exists(os.path.dirname(target_file)): + os.makedirs(os.path.dirname(target_file)) + with open(target_file, 'xb') as f: + f.write(original_zip.read(name)) + except BaseException: + pass + return reference_dir diff --git a/XmindCopilot/core/manifest.py b/XmindCopilot/core/manifest.py new file mode 100644 index 0000000..2a029da --- /dev/null +++ b/XmindCopilot/core/manifest.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python +# _*_ coding:utf-8 _*_ + +""" +XmindCopilot.core.manifest implements encapsulation of the XMind META-INF/manifest.xml. +""" +import random + +from .. import utils +from . import Document, const, Element + + +class ManifestBookDocument(Document): + """ `ManifestBookDocument` as central object correspond XMind manifest file. + + such as: + + + + + + + + + + + + + """ + + def __init__(self, node=None, path=None): + """Construct new `ManifestBookDocument` object + + :param node: pass DOM node object and parse as `ManifestBookDocument` object. + if node not given then created new one. + :param path: set workbook will to be placed. + """ + super(ManifestBookDocument, self).__init__(node) + self._path = path + + _manifestbook_element = self.getFirstChildNodeByTagName(const.TAG_MANIFESTBOOK) + self._manifestbook_element = ManifestBookElement(_manifestbook_element, self) + + if not _manifestbook_element: + self.appendChild(self._manifestbook_element) + + self.setVersion(const.VERSION) + + def getManifestBookElement(self): + return self._manifestbook_element + + def getManifest(self): + return self._manifestbook_element.getManifest() + + def addManifest(self, path, media_type): + return self._manifestbook_element.addManifest(path, media_type) + + +class ManifestBookElement(Element): + """`ManifestBookElement` as the one and only root element of the manifest book document""" + TAG_NAME = const.TAG_MANIFESTBOOK + + def __init__(self, node=None, ownerManifestBook=None): + super(ManifestBookElement, self).__init__(node) + self._owner_manifestbook = ownerManifestBook + self.registerOwnerManifestBook() + self.setAttribute(const.NAMESPACE, const.XMLNS_MANIFEST) + # TODO: password-hint? + + def registerOwnerManifestBook(self): + if self._owner_manifestbook: + self.setOwnerDocument(self._owner_manifestbook.getOwnerDocument()) + + def getOwnerManifestBook(self): + return self._owner_manifestbook + + def getManifest(self): + manifest = self.getChildNodesByTagName(const.TAG_FILE_ENTRY) + owner_manifestbook = self.getOwnerManifestBook() + manifest = [ManifestElement(node=manifest, ownerManifestBook=owner_manifestbook) for manifest in manifest] + return manifest + + def addManifest(self, path, media_type): + manifest = ManifestElement(node=None, ownerManifestBook=self.getOwnerManifestBook()) + manifest.setPath(path) + manifest.setMediaType(media_type) + self.appendChild(manifest) + return manifest + + +class ManifestElement(Element): + """`ManifestElement` as element of the manifest book document""" + TAG_NAME = const.TAG_FILE_ENTRY + + def __init__(self, node=None, ownerManifestBook=None): + super(ManifestElement, self).__init__(node) + self._owner_manifestbook = ownerManifestBook + self.registerOwnerManifestbook() + + def registerOwnerManifestbook(self): + if self._owner_manifestbook: + self.setOwnerDocument(self._owner_manifestbook.getOwnerDocument()) + + def setPath(self, path): + self.setAttribute(const.ATTR_FULL_PATH, path) + + def setMediaType(self, media_type): + self.setAttribute(const.ATTR_MEDIA_TYPE, media_type) + \ No newline at end of file diff --git a/xmind/core/markerref.py b/XmindCopilot/core/markerref.py similarity index 99% rename from xmind/core/markerref.py rename to XmindCopilot/core/markerref.py index 52243ee..46445e1 100644 --- a/xmind/core/markerref.py +++ b/XmindCopilot/core/markerref.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- """ - xmind.core.markerref + XmindCopilot.core.markerref """ from . import const from .mixin import WorkbookMixinElement diff --git a/xmind/core/mixin.py b/XmindCopilot/core/mixin.py similarity index 98% rename from xmind/core/mixin.py rename to XmindCopilot/core/mixin.py index 89190d3..384a6ff 100644 --- a/xmind/core/mixin.py +++ b/XmindCopilot/core/mixin.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- """ - xmind.mixin + XmindCopilot.core.mixin """ from . import const from . import Element diff --git a/xmind/core/notes.py b/XmindCopilot/core/notes.py similarity index 94% rename from xmind/core/notes.py rename to XmindCopilot/core/notes.py index 8ee830b..d4653ef 100644 --- a/xmind/core/notes.py +++ b/XmindCopilot/core/notes.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- """ - xmind.core.notes + XmindCopilot.core.notes """ from . import const from .mixin import TopicMixinElement @@ -46,7 +46,7 @@ class PlainNotes(_NoteContentElement): :param content: utf8 plain text. :param node: `xml.dom.Element` object` - :param ownerTopic: `xmind.core.topic.TopicElement` object + :param ownerTopic: `XmindCopilot.core.topic.TopicElement` object """ diff --git a/xmind/core/position.py b/XmindCopilot/core/position.py similarity index 95% rename from xmind/core/position.py rename to XmindCopilot/core/position.py index 450beeb..801986c 100644 --- a/xmind/core/position.py +++ b/XmindCopilot/core/position.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- """ - xmind.core.position + XmindCopilot.core.position """ from . import const from .mixin import WorkbookMixinElement diff --git a/xmind/core/relationship.py b/XmindCopilot/core/relationship.py similarity index 97% rename from xmind/core/relationship.py rename to XmindCopilot/core/relationship.py index 2891aec..cb9e1a1 100644 --- a/xmind/core/relationship.py +++ b/XmindCopilot/core/relationship.py @@ -2,9 +2,9 @@ # -*- coding: utf-8 -*- """ - xmind.core.relationship + XmindCopilot.core.relationship """ -from xmind import utils +from .. import utils from . import const from .mixin import WorkbookMixinElement from .topic import TopicElement diff --git a/xmind/core/saver.py b/XmindCopilot/core/saver.py similarity index 84% rename from xmind/core/saver.py rename to XmindCopilot/core/saver.py index 57fc648..fd05554 100644 --- a/xmind/core/saver.py +++ b/XmindCopilot/core/saver.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- """ - xmind.core.saver + XmindCopilot.core.saver """ import codecs import os @@ -42,7 +42,17 @@ def _get_styles_xml(self): return styles_path - def _get_reference(self, except_revisions=False): + def _get_manifest_xml(self): + Metainf_dir = os.path.join(self._temp_dir, const.META_INF_DIR) + if not os.path.exists(Metainf_dir): + os.makedirs(Metainf_dir) + manifest_path = utils.join_path(self._temp_dir, const.MANIFEST_XML) + with codecs.open(manifest_path, "w", encoding="utf-8") as f: + self._workbook.manifestbook.output(f) + + return manifest_path + + def _get_origin_reference(self, except_revisions=False): """ Get all references in xmind zip file. @@ -59,7 +69,7 @@ def _get_reference(self, except_revisions=False): try: with original_zip as input_stream: for name in input_stream.namelist(): - if name in [const.CONTENT_XML, const.STYLES_XML, const.COMMENTS_XML]: + if name in [const.CONTENT_XML, const.STYLES_XML, const.COMMENTS_XML, const.MANIFEST_XML]: continue if const.REVISIONS_DIR in name and except_revisions: continue @@ -78,25 +88,29 @@ def save(self, path=None, only_content=False, except_attachments=False, except_r Save the workbook to the given path. If the path is not given, then will save to the path set in workbook. :param path: save to the target path. - :param except_revisions: whether or not to save `Revisions` content to save space. - :param except_attachments: only save content.xml、comments.xml、sytles.xml. :param only_content: only save content.xml + :param except_attachments: only save content.xml、comments.xml、sytles.xml. + :param except_revisions: whether or not to save `Revisions` content to save space. """ original_path = self._workbook.get_path() new_path = path or original_path new_path = utils.get_abs_path(new_path) new_filename, new_suffix = utils.split_ext(new_path) - if new_suffix != const.XMIND_EXT: - raise Exception('XMind filename require a "%s" extension' % const.XMIND_EXT) + if new_suffix != const.XMIND_EXT and new_suffix != const.XMIND8_EXT: + raise Exception('XMind filename require a "%s" or "%s" extension' % {const.XMIND_EXT, const.XMIND8_EXT}) content = self._get_content_xml() if not only_content: styles = self._get_styles_xml() comments = self._get_comments_xml() - if not except_attachments and os.path.exists(original_path): + manifest = self._get_manifest_xml() + if not except_attachments and\ + (os.path.exists(original_path) or self._workbook.reference_dir): is_have_attachments = True - reference_dir = self._get_reference(except_revisions) + reference_dir = self._workbook.reference_dir + if not reference_dir: # save original references if not given + reference_dir = self._get_origin_reference(except_revisions) else: is_have_attachments = False @@ -105,6 +119,7 @@ def save(self, path=None, only_content=False, except_attachments=False, except_r if not only_content: f.write(styles, const.STYLES_XML) f.write(comments, const.COMMENTS_XML) + f.write(manifest, const.MANIFEST_XML) if not except_attachments and is_have_attachments: length = reference_dir.__len__() # the length of the file string for dirpath, dirnames, filenames in os.walk(reference_dir): @@ -162,7 +177,7 @@ def save(self, path=None, only_content=False, except_attachments=False, except_r # content = self._get_content_xml() # styles = self._get_styles_xml() # comments = self._get_comments_xml() - # reference_dir = self._get_reference(original_path) + # reference_dir = self._get_origin_reference(original_path) # # f = utils.compress(new_path) # f.write(content, const.CONTENT_XML) diff --git a/xmind/core/sheet.py b/XmindCopilot/core/sheet.py similarity index 98% rename from xmind/core/sheet.py rename to XmindCopilot/core/sheet.py index 2b39bd5..58575e3 100644 --- a/xmind/core/sheet.py +++ b/XmindCopilot/core/sheet.py @@ -2,9 +2,9 @@ # -*- coding: utf-8 -*- """ -xmind.core.sheet command XMind sheets manipulation +XmindCopilot.core.sheet command XMind sheets manipulation """ -from xmind import utils +from .. import utils from . import const from .mixin import WorkbookMixinElement from .topic import TopicElement diff --git a/xmind/core/styles.py b/XmindCopilot/core/styles.py similarity index 96% rename from xmind/core/styles.py rename to XmindCopilot/core/styles.py index 6ff7ed5..0f42d8e 100644 --- a/xmind/core/styles.py +++ b/XmindCopilot/core/styles.py @@ -2,9 +2,9 @@ # _*_ coding:utf-8 _*_ """ -xmind.core.styles implements encapsulation of the XMind styles.xml. +XmindCopilot.core.styles implements encapsulation of the XMind styles.xml. """ -from xmind.core import Document, const, Element +from . import Document, const, Element class StylesBookDocument(Document): diff --git a/xmind/core/title.py b/XmindCopilot/core/title.py similarity index 71% rename from xmind/core/title.py rename to XmindCopilot/core/title.py index b44ee20..572c25c 100644 --- a/xmind/core/title.py +++ b/XmindCopilot/core/title.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- """ - xmind.core.title + XmindCopilot.core.title """ from . import const @@ -14,3 +14,6 @@ class TitleElement(WorkbookMixinElement): def __init__(self, node=None, ownerWorkbook=None): super(TitleElement, self).__init__(node, ownerWorkbook) + + def setSvgWidth(self, width): + self.setAttribute(const.ATTR_TITLE_SVGWIDTH, width) diff --git a/xmind/core/topic.py b/XmindCopilot/core/topic.py similarity index 51% rename from xmind/core/topic.py rename to XmindCopilot/core/topic.py index 0722076..b949697 100644 --- a/xmind/core/topic.py +++ b/XmindCopilot/core/topic.py @@ -1,491 +1,802 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" - xmind.core.topic -""" -from . import const -from .mixin import WorkbookMixinElement -from .title import TitleElement -from .position import PositionElement -from .notes import NotesElement, PlainNotes -from .labels import LabelsElement, LabelElement -from .markerref import MarkerRefElement -from .markerref import MarkerRefsElement -from .markerref import MarkerId -from .. import utils - - -def split_hyperlink(hyperlink): - colon = hyperlink.find(":") - if colon < 0: - protocol = None - else: - protocol = hyperlink[:colon] - - hyperlink = hyperlink[colon + 1:] - while hyperlink.startswith("/"): - hyperlink = hyperlink[1:] - - return protocol, hyperlink - - -class TopicElement(WorkbookMixinElement): - TAG_NAME = const.TAG_TOPIC - - def __init__(self, node=None, ownerWorkbook=None): - super(TopicElement, self).__init__(node, ownerWorkbook) - - self.addIdAttribute(const.ATTR_ID) - self.setAttribute(const.ATTR_TIMESTAMP, int(utils.get_current_time())) - - def _get_title(self): - return self.getFirstChildNodeByTagName(const.TAG_TITLE) - - def _get_markerrefs(self): - return self.getFirstChildNodeByTagName(const.TAG_MARKERREFS) - - def _get_labels(self): - return self.getFirstChildNodeByTagName(const.TAG_LABELS) - - def _get_notes(self): - return self.getFirstChildNodeByTagName(const.TAG_NOTES) - - def _get_position(self): - return self.getFirstChildNodeByTagName(const.TAG_POSITION) - - def _get_children(self): - return self.getFirstChildNodeByTagName(const.TAG_CHILDREN) - - def _set_hyperlink(self, hyperlink): - self.setAttribute(const.ATTR_HREF, hyperlink) - # self.updateModifiedTime() - - def getOwnerSheet(self): - parent = self.getParentNode() - - while parent and parent.tagName != const.TAG_SHEET: - parent = parent.parentNode - - if not parent: - return - - owner_workbook = self.getOwnerWorkbook() - if not owner_workbook: - return - - for sheet in owner_workbook.getSheets(): - if parent is sheet.getImplementation(): - return sheet - - def getTitle(self): - title = self._get_title() - if title: - title = TitleElement(title, self.getOwnerWorkbook()) - return title.getTextContent() - - def setTitle(self, text): - _title = self._get_title() - title = TitleElement(_title, self.getOwnerWorkbook()) - title.setTextContent(text) - - if _title is None: - self.appendChild(title) - - # self.updateModifiedTime() - - def getMarkers(self): - refs = self._get_markerrefs() - if not refs: - return [] - tmp = MarkerRefsElement(refs, self.getOwnerWorkbook()) - markers = tmp.getChildNodesByTagName(const.TAG_MARKERREF) - marker_list = [] - if markers: - for i in markers: - marker_list.append(MarkerRefElement(i, self.getOwnerWorkbook())) - return marker_list - - def addMarker(self, markerId): - """ - Add a marker to this topic - :param markerId: a markerId indicating the marker to add - :return: a MarkerRefElement instance - """ - if not markerId: - return None - if isinstance(markerId, str): - markerId = MarkerId(markerId) - - refs = self._get_markerrefs() - if not refs: - tmp = MarkerRefsElement(None, self.getOwnerWorkbook()) - self.appendChild(tmp) - else: - tmp = MarkerRefsElement(refs, self.getOwnerWorkbook()) - markers = tmp.getChildNodesByTagName(const.TAG_MARKERREF) - - # If the same family marker exists, replace it - if markers: - for m in markers: - mre = MarkerRefElement(m, self.getOwnerWorkbook()) - # look for a marker of same family - if mre.getMarkerId().getFamily() == markerId.getFamily(): - mre.setMarkerId(markerId) - return mre - # not found so let's append it - mre = MarkerRefElement(None, self.getOwnerWorkbook()) - mre.setMarkerId(markerId) - tmp.appendChild(mre) - return mre - - def getLabels(self): - """ - Get lables content. One topic can set one label right now. - """ - _labels = self._get_labels() - if not _labels: - return None - tmp = LabelsElement(_labels, self) - # labels = tmp.getChildNodesByTagName(const.TAG_LABEL) - # label_list = [] - # if labels: - # for i in labels: - # label_list.append(LabelElement(i, self.getOwnerWorkbook())) - # return label_list - - label = LabelElement(node=tmp.getFirstChildNodeByTagName(const.TAG_LABEL), ownerTopic=self) - content = label.getLabel() - return content - - def addLabel(self, content): - _labels = self._get_labels() - if not _labels: - tmp = LabelsElement(None, self) - self.appendChild(tmp) - else: - tmp = LabelsElement(_labels, self) - old = tmp.getFirstChildNodeByTagName(const.TAG_LABEL) - if old: - tmp.getImplementation().removeChild(old) - - label = LabelElement(content, None, self) - tmp.appendChild(label) - return label - - def getComments(self): - """ - Get comments content. - """ - topic_id = self.getAttribute(const.ATTR_ID) - workbook = self.getOwnerWorkbook() - content = workbook.commentsbook.getComment(topic_id) - return content - - def addComment(self, content, author=None): - topic_id = self.getAttribute(const.ATTR_ID) - workbook = self.getOwnerWorkbook() - comment = workbook.commentsbook.addComment(content=content, topic_id=topic_id, author=author) - return comment - - def getNotes(self): - """ - Get notes content. One topic can set one note right now. - """ - _notes = self._get_notes() - if not _notes: - return None - tmp = NotesElement(_notes, self) - # Only support plain text notes right now - content = tmp.getContent(const.PLAIN_FORMAT_NOTE) - return content - - def setPlainNotes(self, content): - """ Set plain text notes to topic - - :param content: utf8 plain text - """ - new = PlainNotes(content, None, self) - _notes = self._get_notes() - if not _notes: - tmp = NotesElement(None, self) - self.appendChild(tmp) - else: - tmp = NotesElement(_notes, self) - old = tmp.getFirstChildNodeByTagName(new.getFormat()) - if old: - tmp.getImplementation().removeChild(old) - - tmp.appendChild(new) - return new - - def setFolded(self): - self.setAttribute(const.ATTR_BRANCH, const.VAL_FOLDED) - - # self.updateModifiedTime() - - def getPosition(self): - """ Get a pair of integer located topic position. - - return (x, y) indicate x and y - """ - position = self._get_position() - if position is None: - return - - position = PositionElement(position, self.getOwnerWorkbook()) - - x = position.getX() - y = position.getY() - - if x is None and y is None: - return - - x = x or 0 - y = y or 0 - - return int(x), int(y) - - def setPosition(self, x, y): - owner_workbook = self.getOwnerWorkbook() - position = self._get_position() - - if not position: - position = PositionElement(ownerWorkbook=owner_workbook) - self.appendChild(position) - else: - position = PositionElement(position, owner_workbook) - - position.setX(x) - position.setY(y) - # self.updateModifiedTime() - - def removePosition(self): - position = self._get_position() - if position is not None: - self.getImplementation().removeChild(position) - # self.updateModifiedTime() - - def getType(self): - """ - 1、root - 2、attached、detached - """ - parent = self.getParentNode() - if not parent: - return - - if parent.tagName == const.TAG_SHEET: - return const.TOPIC_ROOT - - if parent.tagName == const.TAG_TOPICS: - topics = TopicsElement(parent, self.getOwnerWorkbook()) - return topics.getType() - - def getTopics(self, topics_type=const.TOPIC_ATTACHED): - topic_children = self._get_children() - - if topic_children: - topic_children = ChildrenElement(topic_children, self.getOwnerWorkbook()) - - return topic_children.getTopics(topics_type) - - def getSubTopics(self, topics_type=const.TOPIC_ATTACHED): - """ List all sub topics under current topic, If not sub topics, return empty list. - """ - topics = self.getTopics(topics_type) - if not topics: - return [] - - return topics.getSubTopics() - - def getSubTopicByIndex(self, index, topics_type=const.TOPIC_ATTACHED): - """ Get sub topic by speicifeid index - """ - sub_topics = self.getSubTopics(topics_type) - if sub_topics is None: - return - - if index < 0 or index >= len(sub_topics): - return sub_topics - - return sub_topics[index] - - def addSubTopic(self, topic=None, index=-1, topics_type=const.TOPIC_ATTACHED): - """ - Add a sub topic to the current topic and return added sub topic - - :param topic: `TopicElement` object. If not `TopicElement` object - passed then created new one automatically. - :param index: if index not given then passed topic will append to - sub topics list. Otherwise, index must be less than - length of sub topics list and insert passed topic - before given index. - :param topics_type: TOPIC_ATTACHED or TOPIC_DETACHED - """ - owner_workbook = self.getOwnerWorkbook() - topic = topic or self.__class__(None, owner_workbook) - - topic_children = self._get_children() - if not topic_children: - topic_children = ChildrenElement(ownerWorkbook=owner_workbook) - self.appendChild(topic_children) - else: - topic_children = ChildrenElement(topic_children, owner_workbook) - - topics = topic_children.getTopics(topics_type) - if not topics: - topics = TopicsElement(ownerWorkbook=owner_workbook) - topics.setAttribute(const.ATTR_TYPE, topics_type) - topic_children.appendChild(topics) - - topic_list = [] - for i in topics.getChildNodesByTagName(const.TAG_TOPIC): - topic_list.append(TopicElement(i, owner_workbook)) - - if index < 0 or index >= len(topic_list): - topics.appendChild(topic) - else: - topics.insertBefore(topic, topic_list[index]) - - return topic - - def getIndex(self): - parent = self.getParentNode() - if parent and parent.tagName == const.TAG_TOPICS: - index = 0 - for child in parent.childNodes: - if self.getImplementation() == child: - return index - index += 1 - return -1 - - def getHyperlink(self): - return self.getAttribute(const.ATTR_HREF) - - def setFileHyperlink(self, path): - """ Set file as topic hyperlink - - :param path: path of specified file - - """ - protocol, content = split_hyperlink(path) - if not protocol: - path = const.FILE_PROTOCOL + utils.get_abs_path(path) - - self._set_hyperlink(path) - - def setTopicHyperlink(self, tid): - """ Set topic as topic hyperlink - - :param tid: given topic's id - - """ - protocol, content = split_hyperlink(tid) - if not protocol: - if tid.startswith("#"): - tid = tid[1:] - - tid = const.TOPIC_PROTOCOL + tid - self._set_hyperlink(tid) - - def setURLHyperlink(self, url): - """ Set URL as topic hyperlink - - :param url: HTTP URL to specified website - - """ - protocol, content = split_hyperlink(url) - if not protocol: - url = const.HTTP_PROTOCOL + content - - self._set_hyperlink(url) - - def getStructureClass(self): - self.getAttribute(const.ATTR_STRUCTURE_CLASS) - - def setStructureClass(self, structure_class): - """ Set topic's structure class attribute - - :param structure_class: such as structure-class="org.xmind.ui.map.floating" - - """ - self.setAttribute(const.ATTR_STRUCTURE_CLASS, structure_class) - - def getStyleId(self): - """ Get topic's style id - - :return: such as - """ - return self.getAttribute(const.ATTR_STYLE_ID) - - def setStyleID(self): - style_id = utils.generate_id() - self.setAttribute(const.ATTR_STYLE_ID, style_id) - - def getData(self): - """ Get topic's main content in the form of a dictionary. - if subtopic exist, recursively get the subtopics content. - """ - data = { - 'id': self.getAttribute(const.ATTR_ID), - 'link': self.getAttribute(const.ATTR_HREF), - 'title': self.getTitle(), - 'note': self.getNotes(), - 'label': self.getLabels(), - 'comment': self.getComments(), - 'markers': [marker.getMarkerId().name for marker in self.getMarkers() if marker], - } - - if self.getSubTopics(topics_type=const.TOPIC_ATTACHED): - data['topics'] = [] - for sub_topic in self.getSubTopics(topics_type=const.TOPIC_ATTACHED): - data['topics'].append(sub_topic.getData()) - - return data - - -class ChildrenElement(WorkbookMixinElement): - TAG_NAME = const.TAG_CHILDREN - - def __init__(self, node=None, ownerWorkbook=None): - super(ChildrenElement, self).__init__(node, ownerWorkbook) - - def getTopics(self, topics_type): - topics = self.iterChildNodesByTagName(const.TAG_TOPICS) - for i in topics: - t = TopicsElement(i, self.getOwnerWorkbook()) - if topics_type == t.getType(): - return t - - -class TopicsElement(WorkbookMixinElement): - TAG_NAME = const.TAG_TOPICS - - def __init__(self, node=None, ownerWorkbook=None): - super(TopicsElement, self).__init__(node, ownerWorkbook) - - def getType(self): - return self.getAttribute(const.ATTR_TYPE) - - def getSubTopics(self): - """ - List all sub topics on the current topic - """ - topics = [] - ownerWorkbook = self.getOwnerWorkbook() - for t in self.getChildNodesByTagName(const.TAG_TOPIC): - topics.append(TopicElement(t, ownerWorkbook)) - - return topics - - def getSubTopicByIndex(self, index): - """ - Get specified sub topic by index - """ - sub_topics = self.getSubTopics() - if index < 0 or index >= len(sub_topics): - return sub_topics - - return sub_topics[index] - +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" + XmindCopilot.core.topic +""" +from . import const +from .mixin import WorkbookMixinElement +from .title import TitleElement +from .image import ImageElement +from .position import PositionElement +from .notes import NotesElement, PlainNotes +from .labels import LabelsElement, LabelElement +from .markerref import MarkerRefElement +from .markerref import MarkerRefsElement +from .markerref import MarkerId +from ..fmt_cvt.latex_render import latex2img_web, latex2img_plt +from ..fmt_cvt.md2xmind import MarkDown2Xmind +from ..fmt_cvt.table_render import markdown_table_to_png +from .. import utils +import re +import json + + +def split_hyperlink(hyperlink): + colon = hyperlink.find(":") + if colon < 0: + protocol = None + else: + protocol = hyperlink[:colon] + + hyperlink = hyperlink[colon + 1:] + while hyperlink.startswith("/"): + hyperlink = hyperlink[1:] + + return protocol, hyperlink + + +class TopicElement(WorkbookMixinElement): + TAG_NAME = const.TAG_TOPIC + + def __init__(self, node=None, ownerWorkbook=None, title: str = "", image_path: str = ""): + super(TopicElement, self).__init__(node, ownerWorkbook) + + self.addIdAttribute(const.ATTR_ID) + self.setAttribute(const.ATTR_TIMESTAMP, int(utils.get_current_time())) + if not title == "": + self.setTitle(title) + if not image_path == "": + self.setImage(image_path) + + def _get_title(self): + return self.getFirstChildNodeByTagName(const.TAG_TITLE) + + def _get_image(self): + return self.getFirstChildNodeByTagName(const.TAG_IMAGE) + + def _get_markerrefs(self): + return self.getFirstChildNodeByTagName(const.TAG_MARKERREFS) + + def _get_labels(self): + return self.getFirstChildNodeByTagName(const.TAG_LABELS) + + def _get_notes(self): + return self.getFirstChildNodeByTagName(const.TAG_NOTES) + + def _get_position(self): + return self.getFirstChildNodeByTagName(const.TAG_POSITION) + + def _get_children(self): + return self.getFirstChildNodeByTagName(const.TAG_CHILDREN) + + def _set_hyperlink(self, hyperlink): + self.setAttribute(const.ATTR_HREF, hyperlink) + # self.updateModifiedTime() + + def getOwnerSheet(self): + parent = self.getParentNode() + + while parent and parent.tagName != const.TAG_SHEET: + parent = parent.parentNode + + if not parent: + return + + owner_workbook = self.getOwnerWorkbook() + if not owner_workbook: + return + + for sheet in owner_workbook.getSheets(): + if parent is sheet.getImplementation(): + return sheet + + def getTitle(self): + title = self._get_title() + if title: + title = TitleElement(title, self.getOwnerWorkbook()) + return title.getTextContent() + + def setTitle(self, text): + _title = self._get_title() + title = TitleElement(_title, self.getOwnerWorkbook()) + title.setTextContent(text) + if _title is None: + self.appendChild(title) + # self.updateModifiedTime() + + def setTitleSvgWidth(self, svgwidth=500): + """ + Set svg:width of title of this topic + :param svgwidth: svg:width of title of this topic, default is 500 + """ + _title = self._get_title() + title = TitleElement(_title, self.getOwnerWorkbook()) + title.setSvgWidth(svgwidth) + + def getImage(self): + """Get ImageElement of this topic""" + image_node = self._get_image() + if image_node: + return ImageElement(image_node, self.getOwnerWorkbook()) + + def getImageAttr(self): + image_element = self.getImage() + if image_element: + return image_element._getImgAttribute() + + def setImage(self, img=None, align=None, height=None, width=None): + """ + Set the image and its attr of this topic + + :param img: image path or Image obj to set. If img is None, original img will be reserved. + :param align: image align (["top", "bottom", "left", "right"]). if it is None, it will be removed(Defaults to aligning top). + :param height: image svg:height. If it is None, it will be removed. + :param width: image svg:width. If it is None, it will be removed. + """ + image_element = self.getImage() + if not image_element: + image_element = ImageElement(None, self.getOwnerWorkbook()) + self.appendChild(image_element) + image_element.setImage(img, align, height, width) + + def setLatexEquation(self, latex_equation, align=None, height=None, width=None): + """ + Set the equation as image of this topic + + FIXME: It seems the pyplot latex renderer does not support + $$Latex Block$$ and multi-line latex equation + """ + # latex_equation = latex_equation.replace("$$", "$") + # latex_equation = latex_equation.replace("\n", " ") + # latex_equation = latex_equation.replace("\\\\", "\\") + # trim space and newline + # latex_equation = re.sub(r'^[\s\n]+|[\s\n]+$', '', latex_equation) + # remove \n + latex_equation = re.sub(r'\n', ' ', latex_equation) + try: + # im = latex2img_web(latex_equation) + latex_equation = latex_equation.replace("$$", "") + im = latex2img_plt(latex_equation) + self.setImage(im, align, height, width) + return True + except Exception: + print("Warning: setLatexEquation failed:", latex_equation) + return False + + # For Markdown to Xmind + def convertTitle2Equation(self, align=None, height=None, width=None, recursive=False): + """ + Convert title to latex equation + + :param align: image align (["top", "bottom", "left", "right"]). if it is None, it will be removed(Defaults to aligning top). + :param height: image svg:height. If it is None, it will be removed. + :param width: image svg:width. If it is None, it will be removed. + """ + if recursive: + for c in self.getSubTopics(): + c.convertTitle2Equation(align, height, width, recursive) + title = self.getTitle() + if title: + if re.match(r'^[\s\n]{0,}\$.*?\$[\s\n]{0,}$', title, re.S): + if self.setLatexEquation(title, align, height, width): + self.setTitle("") + + def convertTitle2WebImage(self, align=None, height=None, width=None, recursive=False): + """Convert title to web image + :param align: image align (["top", "bottom", "left", "right"]). if it is None, it will be removed(Defaults to aligning top). + :param height: image svg:height. If it is None, it will be removed. + :param width: image svg:width. If it is None, it will be removed. + :param recursive: if convert sub topics + """ + if recursive: + for c in self.getSubTopics(): + c.convertTitle2WebImage(align, height, width, recursive) + title = self.getTitle() + if title: + # FIXME: + # image-20230706120022138 + # ![]() + # are all should be supported + uriSearch = re.search(r"[\(\"](http[s]{0,1}://.*?)[\)\"]", title) + mdImgMatch = re.match(r'^!\[.*\]\((http[s]{0,1}://.*)\)', title) + htmlDivMatch = re.search(r"img", title) and uriSearch + if mdImgMatch or htmlDivMatch: + try: + self.setImage(uriSearch.group(1), align, height, width) + self.setTitle("") + except: + print("Warning: convertTitle2WebImage failed") + + def convertTitleWithHyperlink(self, recursive=False): + """ + Convert the title with hyperlink to xmind hyperlink + The hyperlink format is [title](url) + """ + if recursive: + for c in self.getSubTopics(): + c.convertTitleWithHyperlink(recursive) + title = self.getTitle() + if title: + strmatch = re.search(r'\[(.*)\]\((.*)\)', title) + if strmatch: + url = strmatch.group(2) + self.setURLHyperlink(url) + self.setTitle(re.sub(r'\[(.*)\]\((.*)\)', r'\1', title)) + + def convertTitle2Table(self, align=None, height=None, width=None, recursive=False): + """ + Convert title to table + """ + if recursive: + for c in self.getSubTopics(): + c.convertTitle2Table(recursive) + title = self.getTitle() + if title: + if re.match(r'^\|.*\|$', title, re.S): + try: + table = markdown_table_to_png(title) + self.setImage(table, align, height, width) + self.setTitle("") + except Exception: + print("Warning: convertTitle2Table failed") + + def getMarkers(self): + refs = self._get_markerrefs() + if not refs: + return [] + tmp = MarkerRefsElement(refs, self.getOwnerWorkbook()) + markers = tmp.getChildNodesByTagName(const.TAG_MARKERREF) + marker_list = [] + if markers: + for i in markers: + marker_list.append(MarkerRefElement( + i, self.getOwnerWorkbook())) + return marker_list + + def addMarker(self, markerId): + """ + Add a marker to this topic + :param markerId: a markerId indicating the marker to add + :return: a MarkerRefElement instance + """ + if not markerId: + return None + if isinstance(markerId, str): + markerId = MarkerId(markerId) + + refs = self._get_markerrefs() + if not refs: + tmp = MarkerRefsElement(None, self.getOwnerWorkbook()) + self.appendChild(tmp) + else: + tmp = MarkerRefsElement(refs, self.getOwnerWorkbook()) + markers = tmp.getChildNodesByTagName(const.TAG_MARKERREF) + + # If the same family marker exists, replace it + if markers: + for m in markers: + mre = MarkerRefElement(m, self.getOwnerWorkbook()) + # look for a marker of same family + if mre.getMarkerId().getFamily() == markerId.getFamily(): + mre.setMarkerId(markerId) + return mre + # not found so let's append it + mre = MarkerRefElement(None, self.getOwnerWorkbook()) + mre.setMarkerId(markerId) + tmp.appendChild(mre) + return mre + + def getLabels(self): + """ + Get lables content. One topic can set one label right now. + """ + _labels = self._get_labels() + if not _labels: + return None + tmp = LabelsElement(_labels, self) + # labels = tmp.getChildNodesByTagName(const.TAG_LABEL) + # label_list = [] + # if labels: + # for i in labels: + # label_list.append(LabelElement(i, self.getOwnerWorkbook())) + # return label_list + + label = LabelElement(node=tmp.getFirstChildNodeByTagName( + const.TAG_LABEL), ownerTopic=self) + content = label.getLabel() + return content + + def addLabel(self, content): + _labels = self._get_labels() + if not _labels: + tmp = LabelsElement(None, self) + self.appendChild(tmp) + else: + tmp = LabelsElement(_labels, self) + old = tmp.getFirstChildNodeByTagName(const.TAG_LABEL) + if old: + tmp.getImplementation().removeChild(old) + + label = LabelElement(content, None, self) + tmp.appendChild(label) + return label + + def getComments(self): + """ + Get comments content. + """ + topic_id = self.getAttribute(const.ATTR_ID) + workbook = self.getOwnerWorkbook() + content = workbook.commentsbook.getComment(topic_id) + return content + + def addComment(self, content, author=None): + topic_id = self.getAttribute(const.ATTR_ID) + workbook = self.getOwnerWorkbook() + comment = workbook.commentsbook.addComment( + content=content, topic_id=topic_id, author=author) + return comment + + def getNotes(self): + """ + Get notes content. One topic can set one note right now. + """ + _notes = self._get_notes() + if not _notes: + return None + tmp = NotesElement(_notes, self) + # TODO: Only support plain text notes right now + content = tmp.getContent(const.PLAIN_FORMAT_NOTE) + return content + + def setPlainNotes(self, content): + """ Set plain text notes to topic + + :param content: utf8 plain text + """ + new = PlainNotes(content, None, self) + _notes = self._get_notes() + if not _notes: + tmp = NotesElement(None, self) + self.appendChild(tmp) + else: + tmp = NotesElement(_notes, self) + old = tmp.getFirstChildNodeByTagName(new.getFormat()) + if old: + tmp.getImplementation().removeChild(old) + + tmp.appendChild(new) + return new + + def setFolded(self, recursive=False): + self.setAttribute(const.ATTR_BRANCH, const.VAL_FOLDED) + if recursive: + for c in self.getSubTopics(): + c.setFolded(recursive=True) + # self.updateModifiedTime() + + def setExpanded(self, recursive=False): + self.setAttribute(const.ATTR_BRANCH, None) + if recursive: + for c in self.getSubTopics(): + c.setExpanded(recursive=True) + # self.updateModifiedTime() + + def getPosition(self): + """ Get a pair of integer located topic position. + + return (x, y) indicate x and y + """ + position = self._get_position() + if position is None: + return + + position = PositionElement(position, self.getOwnerWorkbook()) + + x = position.getX() + y = position.getY() + + if x is None and y is None: + return + + x = x or 0 + y = y or 0 + + return int(x), int(y) + + def setPosition(self, x, y): + owner_workbook = self.getOwnerWorkbook() + position = self._get_position() + + if not position: + position = PositionElement(ownerWorkbook=owner_workbook) + self.appendChild(position) + else: + position = PositionElement(position, owner_workbook) + + position.setX(x) + position.setY(y) + # self.updateModifiedTime() + + def removePosition(self): + position = self._get_position() + if position is not None: + self.getImplementation().removeChild(position) + # self.updateModifiedTime() + + def getType(self): + """ + 1、root + 2、attached、detached + """ + parent = self.getParentNode() + if not parent: + return + + if parent.tagName == const.TAG_SHEET: + return const.TOPIC_ROOT + + if parent.tagName == const.TAG_TOPICS: + topics = TopicsElement(parent, self.getOwnerWorkbook()) + return topics.getType() + + def modify(self, fun, *args, recursive=False): + """modify topic and sub topics. + + :param fun: function to modify topic + :param args: args for fun + :param kwargs: kwargs for fun + :param recursive: if modify sub topics + """ + fun(self, *args) + if recursive: + for c in self.getSubTopics(): + c.modify(fun, *args, recursive=recursive) + + # 获取单层子主题(TopicsElement形式返回 节点仍然在本层) + def getTopics(self, topics_type=const.TOPIC_ATTACHED): + topic_children = self._get_children() + + if topic_children: + topic_children = ChildrenElement( + topic_children, self.getOwnerWorkbook()) + + return topic_children.getTopics(topics_type) + + # 获取单层子主题(list形式返回) + def getSubTopics(self, topics_type=const.TOPIC_ATTACHED): + """ List all sub topics under current topic, If not sub topics, return empty list. + """ + topics = self.getTopics(topics_type) + if not topics: + return [] + + return topics.getSubTopics() + + # 根据引索获取子主题 + def getSubTopicByIndex(self, index, topics_type=const.TOPIC_ATTACHED): + """ Get sub topic by speicifeid index + """ + sub_topics = self.getSubTopics(topics_type) + if sub_topics is None: + return + + if index < 0 or index >= len(sub_topics): + return sub_topics + + return sub_topics[index] + + # 增加子主题 + def addSubTopic(self, topic=None, index=-1, topics_type=const.TOPIC_ATTACHED, svg_width=500): + """ + Add a sub topic to the current topic and return added sub topic + + :param topic: `TopicElement` object. If not `TopicElement` object + passed then created new one automatically. + :param index: if index not given then passed topic will append to + sub topics list. Otherwise, index must be less than + length of sub topics list and insert passed topic + before given index. + :param topics_type: TOPIC_ATTACHED or TOPIC_DETACHED + :param svg_width: svg width (default 500) + :return: added sub topic + """ + owner_workbook = self.getOwnerWorkbook() + topic = topic or self.__class__(None, owner_workbook) + + topic_children = self._get_children() + if not topic_children: + topic_children = ChildrenElement(ownerWorkbook=owner_workbook) + self.appendChild(topic_children) + else: + topic_children = ChildrenElement(topic_children, owner_workbook) + + topics = topic_children.getTopics(topics_type) + if not topics: + topics = TopicsElement(ownerWorkbook=owner_workbook) + topics.setAttribute(const.ATTR_TYPE, topics_type) + topic_children.appendChild(topics) + + topic_list = [] + for i in topics.getChildNodesByTagName(const.TAG_TOPIC): + topic_list.append(TopicElement(i, owner_workbook)) + + if index < 0 or index >= len(topic_list): + topics.appendChild(topic) + else: + topics.insertBefore(topic, topic_list[index]) + topic.setTitleSvgWidth(svg_width) + return topic + + def addSubTopicbyTitle(self, title, index=-1): + return self.addSubTopic(TopicElement(ownerWorkbook=self.getOwnerWorkbook(), title=title), index) + + def addSubTopicbyList(self, content_list, index=-1): + if index < 0: + for item in content_list: + self.addSubTopicbyTitle(item) + else: + for i in range(len(content_list)): + self.addSubTopicbyTitle(content_list[i], index+i) + + def addSubTopicbyIndentedList(self, content_list, index=-1): + """ + Add subtopic tree to the current topic judging by '\t' prefix in each + :param content_list: list of string + :param index: insert index + """ + minIndent = None + last = None + for i in range(len(content_list)): + item = content_list[i] + indent = re.match(r'[\t]{0,}', item).group().count('\t') + # if indent == 0: + # pindex = index + # else: + # pindex = -1 + if minIndent is None or indent <= minIndent: + minIndent = indent + if last is not None: + subtopic = self.addSubTopicbyTitle( + content_list[last].strip('\t'), index) + if index >= 0: + index += 1 + subtopic.addSubTopicbyIndentedList( + content_list[last+1:i], -1) + last = i + if i == len(content_list) - 1: + subtopic = self.addSubTopicbyTitle( + content_list[last].strip('\t'), index) + subtopic.addSubTopicbyIndentedList(content_list[last+1:], -1) + + def addSubTopicbyMarkDown(self, mdtext, cvtEquation=False, cvtWebImage=False, index=-1): + MarkDown2Xmind(self).convert2xmind( + mdtext, cvtEquation, cvtWebImage, index) + + def addSubTopicbyImage(self, image_path, index=-1): + return self.addSubTopic(TopicElement(ownerWorkbook=self.getOwnerWorkbook(), + image_path=image_path), index) + + def removeTopic(self): + """Remove(Detach) self from parent topic""" + self.getParentNode().removeChild(self.getImplementation()) + + def removeSubTopic(self): + """Remove all sub topics""" + topics = self.getSubTopics() + for t in topics: + t.removeTopic() + + def removeSubTopicbyMarkerId(self, markerId, recursive=False): + topics = self.getSubTopics() + for t in topics: + if recursive: + t.removeSubTopicbyMarkerId(markerId, recursive) + for m in t.getMarkers(): + if m.getMarkerId().name == markerId: + t.removeTopic() + + def removeSubTopicbyTitle(self, title, recursive=False): + topics = self.getSubTopics() + for t in topics: + if recursive: + t.removeSubTopicbyTitle(title, recursive) + if t.getTitle() == title: + t.removeTopic() + + def removeSubTopicWithEmptyTitle(self, recursive=True): + """Remove sub topic with empty title(reserved for image)""" + topics = self.getSubTopics() + for t in topics: + if recursive: + t.removeSubTopicWithEmptyTitle(recursive) + if (t.getTitle() is None or re.match(r'^[\t\s]{0,}$', t.getTitle())) and t.getImage() is None: + t.removeTopic() + + def moveTopic(self, index): + ''' + description: Move SubTopic to index\n + param {*} self\n + param {*} index - -1: move to last\n + return {*} + ''' + owner_workbook = self.getOwnerWorkbook() + parent_topic = self.getParentTopic() + topic_children = parent_topic._get_children() + if not topic_children: + topic_children = ChildrenElement(ownerWorkbook=owner_workbook) + self.appendChild(topic_children) + else: + topic_children = ChildrenElement(topic_children, owner_workbook) + topics = topic_children.getTopics(const.TOPIC_ATTACHED) + topic_list = [] + for i in topics.getChildNodesByTagName(const.TAG_TOPIC): + topic_list.append(TopicElement(i, owner_workbook)) + if index >= 0 and index < len(topic_list): + # TODO: Why don't need to remove origin topic?(and the moved topic will not be duplicated) + # self.removeTopic() + topics.insertBefore(self, topic_list[index]) + elif index == -1: + topics.appendChild(self) + + # 获取自身引索 + def getIndex(self): + parent = self.getParentNode() + if parent and parent.tagName == const.TAG_TOPICS: + index = 0 + for child in parent.childNodes: + if self.getImplementation() == child: + return index + index += 1 + return -1 + + def getHyperlink(self): + return self.getAttribute(const.ATTR_HREF) + + def setHyperlink(self, hyperlink: str): + """ Set hyperlink string directly to topic + + :param hyperlink: topic hyperlink + :return: None + """ + self._set_hyperlink(hyperlink) + + def setFileHyperlink(self, path): + """ Set file as topic hyperlink + + :param path: path of specified file + + """ + protocol, content = split_hyperlink(path) + if not protocol: + path = const.FILE_PROTOCOL + utils.get_abs_path(path) + + self._set_hyperlink(path) + + def setTopicHyperlink(self, tid): + """ Set topic as topic hyperlink + + :param tid: given topic's id + + """ + protocol, content = split_hyperlink(tid) + if not protocol: + if tid.startswith("#"): + tid = tid[1:] + + tid = const.TOPIC_PROTOCOL + tid + self._set_hyperlink(tid) + + def setURLHyperlink(self, url): + """ Set URL as topic hyperlink + + :param url: HTTP URL to specified website + + """ + protocol, content = split_hyperlink(url) + if not protocol: + url = const.HTTP_PROTOCOL + content + + self._set_hyperlink(url) + + def getStructureClass(self): + self.getAttribute(const.ATTR_STRUCTURE_CLASS) + + def setStructureClass(self, structure_class): + """ Set topic's structure class attribute + + :param structure_class: such as structure-class="org.xmind.ui.map.floating" + + """ + self.setAttribute(const.ATTR_STRUCTURE_CLASS, structure_class) + + def getStyleId(self): + """ Get topic's style id + + :return: such as + """ + return self.getAttribute(const.ATTR_STYLE_ID) + + def setStyleID(self): + style_id = utils.generate_id() + self.setAttribute(const.ATTR_STYLE_ID, style_id) + + def getData(self): + """ Get topic's main content in the form of a dictionary. + if subtopic exist, recursively get the subtopics content. + """ + data = { + 'id': self.getAttribute(const.ATTR_ID), + 'link': self.getAttribute(const.ATTR_HREF), + 'title': self.getTitle(), + 'note': self.getNotes(), + 'label': self.getLabels(), + 'comment': self.getComments(), + 'markers': [marker.getMarkerId().name for marker in self.getMarkers() if marker], + } + + if self.getSubTopics(topics_type=const.TOPIC_ATTACHED): + data['topics'] = [] + for sub_topic in self.getSubTopics(topics_type=const.TOPIC_ATTACHED): + data['topics'].append(sub_topic.getData()) + + return data + + def to_prettify_json(self): + """ + Convert the contents of the workbook to a json format + """ + return json.dumps(self.getData(), indent=4, separators=(',', ': '), ensure_ascii=False) + + def getParentTopic(self): + pnode = self._node.parentNode + for i in range(2): + pnode = pnode.parentNode + return TopicElement(pnode, self._owner_workbook) + + +class ChildrenElement(WorkbookMixinElement): + TAG_NAME = const.TAG_CHILDREN + + def __init__(self, node=None, ownerWorkbook=None): + super(ChildrenElement, self).__init__(node, ownerWorkbook) + + def getTopics(self, topics_type): + topics = self.iterChildNodesByTagName(const.TAG_TOPICS) + for i in topics: + t = TopicsElement(i, self.getOwnerWorkbook()) + if topics_type == t.getType(): + return t + + +class TopicsElement(WorkbookMixinElement): + TAG_NAME = const.TAG_TOPICS + + def __init__(self, node=None, ownerWorkbook=None): + super(TopicsElement, self).__init__(node, ownerWorkbook) + + def getType(self): + return self.getAttribute(const.ATTR_TYPE) + + # 将topics组转化成topics列表 + def getSubTopics(self): + """ + List all sub topics on the current topic + """ + topics = [] + ownerWorkbook = self.getOwnerWorkbook() + for t in self.getChildNodesByTagName(const.TAG_TOPIC): + topics.append(TopicElement(t, ownerWorkbook)) + + return topics + + def getSubTopicByIndex(self, index): + """ + Get specified sub topic by index + """ + sub_topics = self.getSubTopics() + if index < 0 or index >= len(sub_topics): + return sub_topics + + return sub_topics[index] diff --git a/xmind/core/workbook.py b/XmindCopilot/core/workbook.py similarity index 93% rename from xmind/core/workbook.py rename to XmindCopilot/core/workbook.py index 00d78f3..9742f54 100644 --- a/xmind/core/workbook.py +++ b/XmindCopilot/core/workbook.py @@ -2,10 +2,10 @@ # -*- coding: utf-8 -*- """ -xmind.core.workbook implements the command XMind manipulations. +XmindCopilot.core.workbook implements the command XMind manipulations. """ import json - +import os from . import Document from . import const from .mixin import WorkbookMixinElement @@ -108,7 +108,8 @@ class WorkbookDocument(Document): """ `WorkbookDocument` as central object correspond XMind workbook. """ - def __init__(self, node=None, path=None, stylesbook=None, commentsbook=None): + def __init__(self, node=None, path=None, stylesbook=None, commentsbook=None, + manifestbook=None, reference_dir=None): """Construct new `WorkbookDocument` object :param node: pass DOM node object and parse as `WorkbookDocument` object. @@ -121,6 +122,8 @@ def __init__(self, node=None, path=None, stylesbook=None, commentsbook=None): self._path = path self.stylesbook = stylesbook self.commentsbook = commentsbook + self.manifestbook = manifestbook + self.reference_dir = reference_dir # Initialize WorkbookDocument to make sure that contains WorkbookElement as root. _workbook_element = self.getFirstChildNodeByTagName(const.TAG_WORKBOOK) @@ -243,3 +246,10 @@ def to_prettify_json(self): Convert the contents of the workbook to a json format """ return json.dumps(self.getData(), indent=4, separators=(',', ': '), ensure_ascii=False) + + def get_attachments_path(self): + """Get temp attachments path under reference directory""" + attach_path = os.path.join(self.reference_dir, "attachments") + if not os.path.isdir(attach_path): + os.makedirs(attach_path) + return attach_path \ No newline at end of file diff --git a/XmindCopilot/file_shrink/__init__.py b/XmindCopilot/file_shrink/__init__.py new file mode 100644 index 0000000..5d3ade7 --- /dev/null +++ b/XmindCopilot/file_shrink/__init__.py @@ -0,0 +1,178 @@ +import cv2 +import os +import numpy as np +import subprocess +import zipfile +from glob import glob +import shutil +import tempfile +from tqdm import trange + +# Directory Management +try: + # Run in Terminal + ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) +except Warning: + # Run in ipykernel & interactive + ROOT_DIR = os.getcwd() +# TMP_DIR = os.path.join(ROOT_DIR, "temp") +TMP_DIR = tempfile.mkdtemp() + +OUTPUT_DISPLAY = False + + +def debug_print(*args): + if OUTPUT_DISPLAY: + print(*args) + +def pngquant_compress(fp, force=False, quality=None): + ''' + description: Compress png images using pngquant.exe + param {*} fp: file path + param {*} force: whether to overwrite existing files (default behavior) (default: False) + param {*} quality: 1-100(low-high) + ''' + force_command = '-f' if force else '' + + quality_command = '' + if quality and isinstance(quality, int): + quality_command = f'--quality {quality}' + if quality and isinstance(quality, str): + quality_command = f'--quality {quality}' + + if os.path.isfile(fp): + command = ROOT_DIR + \ + f'/pngquant/pngquant.exe \"{fp}\" --skip-if-larger {force_command} {quality_command} --ext=.png' + subprocess.run(command) + elif os.path.isdir(fp): + command = ROOT_DIR + \ + f'/pngquant/pngquant.exe \"{fp}\"\\*.png --skip-if-larger {force_command} {quality_command} --ext=.png' + subprocess.run(command) + else: + debug_print(f'Warning: {fp} is not a file or directory.') + + +def shrink_images(folder_path, PNG_Quality, JPEG_Quality, use_pngquant=True): + # Get the list of files in the folder + files = os.listdir(folder_path) + + # batch compress png + debug_print("Shrinking png images...") + if use_pngquant: + debug_print("pngquant(no progress bar)") + pngquant_compress(folder_path, force=True, quality=PNG_Quality) + else: + if OUTPUT_DISPLAY: progress = trange(len(files)) + for i in range(len(files)): + file = files[i] + if OUTPUT_DISPLAY: progress.update(1) + image_path = os.path.join(folder_path, file) + if file.endswith('.png'): + # Support Chinese path + image = cv2.imdecode(np.fromfile(image_path, dtype=np.uint8), -1) + cv2.imencode(".png", image, [cv2.IMWRITE_PNG_COMPRESSION, + PNG_Quality])[1].tofile(image_path) + # batch compress jpg + debug_print("Shrinking jpg images...") + if OUTPUT_DISPLAY: progress = trange(len(files)) + for i in range(len(files)): + file = files[i] + if OUTPUT_DISPLAY: progress.update(1) + image_path = os.path.join(folder_path, file) + + if file.endswith('.jpg') or file.endswith('.jpeg'): + # Support Chinese path + image = cv2.imdecode(np.fromfile(image_path, dtype=np.uint8), -1) + cv2.imencode(".jpg", image, [cv2.IMWRITE_JPEG_QUALITY, + JPEG_Quality])[1].tofile(image_path) + # elif file.endswith('.png'): + # pngquant_compress(image_path, force=True, quality=PNG_Quality) + + +def zipDir(dirpath, outFullName): + """ + 压缩指定文件夹 + :param dirpath: 目标文件夹路径 + :param outFullName: 压缩文件保存路径+xxxx.zip + :return: 无 + """ + zip = zipfile.ZipFile(outFullName, "w", zipfile.ZIP_DEFLATED) + for path, dirnames, filenames in os.walk(dirpath): + # 去掉目标跟路径,只对目标文件夹下边的文件及文件夹进行压缩 + fpath = path.replace(dirpath, '') + + for filename in filenames: + zip.write(os.path.join(path, filename), + os.path.join(fpath, filename)) + zip.close() + + +def xmind_shrink(path, PNG_Quality=10, JPEG_Quality=20, use_pngquant=True, + replace=True, output_path=None): + """ + Shrinking xmind file(s) + :param path: xmind file path or folder path containing the xmind files + :param PNG_Quality: CV: 0-9(high-low) | pngquant: 1-100(low-high) + :param JPEG_Quality: CV: 0-100(low-high) + :param use_pngquant: whether to use pngquant.exe to compress png images (default: True) + :param replace: whether to replace the original file (default: True) + :param output_path: If replace is False, compress to the output_path (default: None). + If None, the output path is the same as the original file path, with the suffix ".shrink.xmind" added. + """ + + xmind_files = [] + if path is None: + debug_print("Please specify the path of the xmind file or folder containing the xmind files.") + return + if os.path.isfile(path): + xmind_files = [path] + elif os.path.isdir(path): + xmind_files = glob(path+'/**/*.xmind', recursive=True) + + debug_print("Xmind Files:") + for i in range(len(xmind_files)): + debug_print(f'{i+1}: {xmind_files[i]}') + debug_print('\n') + + for file in xmind_files: + if file.endswith('.shrink.xmind'): + continue + debug_print('Shrinking No.%02d: %s' % (xmind_files.index(file)+1, file)) + if os.path.exists(TMP_DIR): + shutil.rmtree(TMP_DIR) + zip = zipfile.ZipFile(file) + zip.extractall(TMP_DIR) + zip.close() + if os.path.exists(os.path.join(TMP_DIR, "attachments")): + shrink_images(os.path.join(TMP_DIR, "attachments"), + PNG_Quality, JPEG_Quality, use_pngquant=use_pngquant) + if replace: + zipDir(TMP_DIR, file) + else: + if output_path is None: + output_path = file+".shrink.xmind" + zipDir(TMP_DIR, output_path) + else: + debug_print(f'No images found in: {file}') + shutil.rmtree(TMP_DIR) + + +if __name__ == "__main__": + # Specify the OR + # folder_path = "D:\\CodeTestFiles\\HITSA-Courses-Xmind-Note" + folder_path = "E:\\Temp\\Player One.xmind" + + # Specify the compression level + use_pngquant = True + # CV: 0-9(high-low) | pngquant: 1-100(low-high) + PNG_Quality = 10 + # CV: 0-100(low-high) + JPEG_Quality = 20 + + ''' + ideal for xmind files: PNG_Quality=10, JPEG_Quality=20 + extreme compression: PNG_Quality=1, JPEG_Quality=0 (PNG will lose color(almost B&W?), JPEG will lose color details) + ''' + OUTPUT_DISPLAY = True + xmind_shrink(folder_path, PNG_Quality, JPEG_Quality, replace=True, + use_pngquant=use_pngquant) diff --git a/XmindCopilot/file_shrink/pngquant/COPYRIGHT b/XmindCopilot/file_shrink/pngquant/COPYRIGHT new file mode 100644 index 0000000..e4c9b4e --- /dev/null +++ b/XmindCopilot/file_shrink/pngquant/COPYRIGHT @@ -0,0 +1,687 @@ + +pngquant and libimagequant are derived from code by Jef Poskanzer and Greg Roelofs +licensed under pngquant's original licenses (near the end of this file), +and contain extensive changes and additions by Kornel Lesiński +licensed under GPL v3 or later. + +- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +pngquant © 2009-2018 by Kornel Lesiński. + + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + +- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +The quantization and dithering code in pngquant is lifted from Jef Poskanzer's +'ppmquant', part of his wonderful PBMPLUS tool suite. + +Greg Roelofs hacked it into a (in his words) "slightly cheesy" 'pamquant' back +in 1997 (see http://pobox.com/~newt/greg_rgba.html) and finally he ripped out +the cheesy file-I/O parts and replaced them with nice PNG code in December +2000. The PNG reading and writing code is a merged and slightly simplified +version of readpng, readpng2, and writepng from his book "PNG: The Definitive +Guide." +In 2014 Greg has relicensed the code under the simplified BSD license. + +Note that both licenses are basically BSD-like; that is, use the code however +you like, as long as you acknowledge its origins. + +- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +pngquant.c: + + © 1989, 1991 by Jef Poskanzer. + + Permission to use, copy, modify, and distribute this software and its + documentation for any purpose and without fee is hereby granted, provided + that the above copyright notice appear in all copies and that both that + copyright notice and this permission notice appear in supporting + documentation. This software is provided "as is" without express or + implied warranty. + +- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +pngquant.c and rwpng.c/h: + + © 1997-2002 by Greg Roelofs; based on an idea by Stefan Schneider. + + All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/XmindCopilot/file_shrink/pngquant/Drag PNG here to reduce palette automatically.bat b/XmindCopilot/file_shrink/pngquant/Drag PNG here to reduce palette automatically.bat new file mode 100644 index 0000000..b0ca3d0 --- /dev/null +++ b/XmindCopilot/file_shrink/pngquant/Drag PNG here to reduce palette automatically.bat @@ -0,0 +1,11 @@ +@echo off + +set path=%~d0%~p0 + +:start + +"%path%pngquant.exe" --force --verbose --quality=45-85 %1 +"%path%pngquant.exe" --force --verbose --ordered --speed=1 --quality=50-90 %1 + +shift +if NOT x%1==x goto start diff --git a/XmindCopilot/file_shrink/pngquant/Drag PNG here to reduce palette to 256.bat b/XmindCopilot/file_shrink/pngquant/Drag PNG here to reduce palette to 256.bat new file mode 100644 index 0000000..07b8acf --- /dev/null +++ b/XmindCopilot/file_shrink/pngquant/Drag PNG here to reduce palette to 256.bat @@ -0,0 +1,10 @@ +@echo off + +set path=%~d0%~p0 + +:start + +"%path%pngquant.exe" --force --verbose 256 %1 + +shift +if NOT x%1==x goto start diff --git a/XmindCopilot/file_shrink/pngquant/README.txt b/XmindCopilot/file_shrink/pngquant/README.txt new file mode 100644 index 0000000..bbb28e8 --- /dev/null +++ b/XmindCopilot/file_shrink/pngquant/README.txt @@ -0,0 +1,78 @@ +# pngquant 2 + +[pngquant](https://pngquant.org) is a PNG compresor that significantly reduces file sizes by converting images to a more efficient 8-bit PNG format *with alpha channel* (often 60-80% smaller than 24/32-bit PNG files). Compressed images are fully standards-compliant and are supported by all web browsers and operating systems. + +[This](https://github.com/kornelski/pngquant) is the official `pngquant` repository. The compression engine is also available [as an embeddable library](https://github.com/ImageOptim/libimagequant). + +## Usage + +- batch conversion of multiple files: `pngquant *.png` +- Unix-style stdin/stdout chaining: `… | pngquant - | …` + +To further reduce file size, try [optipng](http://optipng.sourceforge.net), [ImageOptim](https://imageoptim.com), or [zopflipng](https://github.com/google/zopfli). + +## Features + + * High-quality palette generation + - advanced quantization algorithm with support for gamma correction and premultiplied alpha + - unique dithering algorithm that does not add unnecessary noise to the image + + * Configurable quality level + - automatically finds required number of colors and can skip images which can't be converted with the desired quality + + * Fast, modern code + - based on a portable [libimagequant library](https://github.com/ImageOptim/libimagequant) + - C99 with no workarounds for legacy systems or compilers ([apart from Visual Studio](https://github.com/kornelski/pngquant/tree/msvc)) + - multicore support (via OpenMP) and Intel SSE optimizations + +## Options + +See `pngquant -h` for full list. + +### `--quality min-max` + +`min` and `max` are numbers in range 0 (worst) to 100 (perfect), similar to JPEG. pngquant will use the least amount of colors required to meet or exceed the `max` quality. If conversion results in quality below the `min` quality the image won't be saved (if outputting to stdin, 24-bit original will be output) and pngquant will exit with status code 99. + + pngquant --quality=65-80 image.png + +### `--ext new.png` + +Set custom extension (suffix) for output filename. By default `-or8.png` or `-fs8.png` is used. If you use `--ext=.png --force` options pngquant will overwrite input files in place (use with caution). + +### `-o out.png` or `--output out.png` + +Writes converted file to the given path. When this option is used only single input file is allowed. + +### `--skip-if-larger` + +Don't write converted files if the conversion isn't worth it. + +### `--speed N` + +Speed/quality trade-off from 1 (slowest, highest quality, smallest files) to 11 (fastest, less consistent quality, light comperssion). The default is 3. It's recommended to keep the default, unless you need to generate images in real time (e.g. map tiles). Higher speeds are fine with 256 colors, but don't handle lower number of colors well. + +### `--nofs` + +Disables Floyd-Steinberg dithering. + +### `--floyd=0.5` + +Controls level of dithering (0 = none, 1 = full). Note that the `=` character is required. + +### `--posterize bits` + +Reduce precision of the palette by number of bits. Use when the image will be displayed on low-depth screens (e.g. 16-bit displays or compressed textures in ARGB444 format). + +### `--strip` + +Don't copy optional PNG chunks. Metadata is always removed on Mac (when using Cocoa reader). + +See [man page](https://github.com/kornelski/pngquant/blob/master/pngquant.1) (`man pngquant`) for the full list of options. + +## License + +pngquant is dual-licensed: + +* Under **GPL v3** or later with an additional [copyright notice](https://github.com/kornelski/pngquant/blob/master/COPYRIGHT) that must be kept for the older parts of the code. + +* Or [a **commercial license**](https://supportedsource.org/projects/pngquant) for use in non-GPL software (e.g. closed-source or App Store distribution). You can [get the license via Supported Source](https://supportedsource.org/projects/pngquant/purchase). Email kornel@pngquant.org if you have any questions. diff --git a/XmindCopilot/file_shrink/pngquant/pngquant.exe b/XmindCopilot/file_shrink/pngquant/pngquant.exe new file mode 100644 index 0000000..8637615 Binary files /dev/null and b/XmindCopilot/file_shrink/pngquant/pngquant.exe differ diff --git a/example/__init__.py b/XmindCopilot/fmt_cvt/__init__.py similarity index 100% rename from example/__init__.py rename to XmindCopilot/fmt_cvt/__init__.py diff --git a/XmindCopilot/fmt_cvt/latex_render.py b/XmindCopilot/fmt_cvt/latex_render.py new file mode 100644 index 0000000..d18ccdb --- /dev/null +++ b/XmindCopilot/fmt_cvt/latex_render.py @@ -0,0 +1,132 @@ +# -*- coding: utf-8 -*- + +import os +from io import BytesIO +from PIL import Image +import numpy as np +import matplotlib +matplotlib.use('Agg') +import matplotlib.font_manager as mfm +import matplotlib.pyplot as plt +from matplotlib import mathtext +import requests +import tempfile +from ..utils import generate_id + + +TEMP_DIR = tempfile.gettempdir() + +# DEPRECATED + + +def latex2img(text, size=32, color=(0.0, 0.0, 0.0), out=None, **kwds): + """ + Convert LaTeX Mathematical Formulas to Images using mathtext + + :param text: Text string containing mathematical formulas enclosed between two dollar signs + :param size: Font size, integer, default is 32 + :param color: Color, tuple of three floating-point values in the range [0, 1], default is black + :param out: File name, only supports filenames with the .png extension. If None, a PIL image object will be returned. + :param kwds: Keyword arguments + dpi: Output resolution in dots per inch (DPI), default is 72 + family: System-supported font, None for the current default font + weight: Stroke weight, options include: normal (default), light, and bold + """ + + assert out is None or os.path.splitext( + out)[1].lower() == '.png', 'Only supports filenames with the .png extension' + + for key in kwds: + if key not in ['dpi', 'family', 'weight']: + raise KeyError('key is not supported:%s' % key) + + dpi = kwds.get('dpi', 72) + family = kwds.get('family', None) + weight = kwds.get('weight', 'normal') + + bfo = BytesIO() # file-like object + prop = mfm.FontProperties(family=family, size=size, weight=weight) + mathtext.math_to_image(text, bfo, prop=prop, dpi=dpi) + im = Image.open(bfo) + + r, g, b, a = im.split() + r, g, b = 255-np.array(r), 255-np.array(g), 255-np.array(b) + a = r/3 + g/3 + b/3 + r, g, b = r*color[0], g*color[1], b*color[2] + + im = np.dstack((r, g, b, a)).astype(np.uint8) + im = Image.fromarray(im) + + if out is None: + return im + else: + im.save(out) + # print('File is saved to %s' % out) + + +def latex2img_web(expression, output_file=None, padding=10, image_format='png', verbose=False): + """ + Convert LaTeX Mathematical Formulas to Images using mathtext + + :param expression: Text string containing mathematical formulas (NOT enclosed between two dollar signs) + :param output_file: File name, only supports filenames with the .png extension. If None, a PIL image object will be returned. + :param padding: Padding, integer, default is 10 + :param image_format: Image format, string, default is 'png' + :param verbose: Whether to print verbose information, boolean, default is False + :return: File path of the generated image + """ + # base_url = "https://tools.timodenk.com" + base_url = "http://localhost:3000" + expression = expression.replace("$", "") # Remove dollar signs + endpoint = f"/api/tex2img/{expression}" + query_params = {'padding': padding, 'format': image_format} + + vprint = print if verbose else lambda *a, **k: None + + response = requests.get(f"{base_url}{endpoint}", params=query_params, verify=False) + + if response.status_code == 200: + content_type = response.headers['Content-Type'] + if 'svg' in content_type: + file_extension = 'svg' + elif 'jpeg' in content_type: + file_extension = 'jpg' + else: + file_extension = image_format + if output_file is None: + print(TEMP_DIR) + output_file = os.path.join(TEMP_DIR, generate_id() + f".{file_extension}") + with open(output_file, 'wb') as f: + f.write(response.content) + vprint(f"Equation rendered and saved as {output_file}") + return output_file + elif response.status_code == 414: + vprint("Request-URI Too Long: The expression exceeded the maximum length") + elif response.status_code == 500: + vprint("Internal Server Error: Conversion failed due to invalid TeX code") + else: + vprint(f"An error occurred with status code: {response.status_code}") + + +def latex2img_plt(formula, filename=None, fontsize=20, dpi=300): + file_extension = "png" + # 配置LaTeX渲染引擎 + plt.rcParams["mathtext.fontset"] = "cm" # 使用Computer Modern字体 + + # 创建虚拟图像 + # fig = plt.figure(figsize=(0.1, 0.1)) + fig = plt.figure() + fig.text(0, 0, f"${formula}$", fontsize=fontsize) + + if filename is None: + filename = os.path.join(TEMP_DIR, generate_id() + f".{file_extension}") + + # 保存为图片 + plt.savefig(filename, dpi=dpi, bbox_inches='tight', pad_inches=0.02) + plt.close() + return filename + + +if __name__ == "__main__": + # 使用示例 + latex2img_plt(r"\frac{\partial J}{\partial \theta} = \sum_{i=1}^n (h_\theta(x^{(i)}) - y^{(i)})x_j^{(i)}", "equation.png") diff --git a/XmindCopilot/fmt_cvt/md2xmind.py b/XmindCopilot/fmt_cvt/md2xmind.py new file mode 100644 index 0000000..0d6fba1 --- /dev/null +++ b/XmindCopilot/fmt_cvt/md2xmind.py @@ -0,0 +1,261 @@ +import re + + +class MDSection(object): + """Markdown Section Class + --- + Mangage the markdown sections identified by `#` + """ + + titleLineMatchStr = r"\s{0,3}(#{1,6})\s{1,}(.*)" + # FIXME: Seems level 1 is not found + listLineMatchStr = r"(\s{0,})(\d{1,}\.|[+*-])\s{1,}(.*)" + + def __init__(self, title: str = "", text: str = ""): + """ + :param title: Title + :param text: Text (Should not contain title) + """ + self.title = title + self.text = text + self.textList = text.strip('\n').split('\n') + self.nonSubSectionText = '' + self.nonSubSectionTextList = [] + self.SubSection = [] + self.segment() + + def _getTitleLevel(self, line): + """Get the level of the title + """ + titleMatch = re.match(self.titleLineMatchStr, line) + if titleMatch: + return len(titleMatch.groups()[0]) + else: + return None + + def _getListLevel(self, line, indent=2): + """Get the level of the numbered list + """ + listmatch = re.match(self.listLineMatchStr, line) + if listmatch: + return len(listmatch.groups()[0])//indent + else: + return None + + def segment(self): + """Segment the text into sub-sections (according to the title level) + """ + majorTitleLevel = 6 # The major title level of the segment + lasti = None + code_block_flag = False + for i in range(len(self.textList)): # i: line index + line = self.textList[i] + # Segment line ignore + if re.match(r"---", line) and i != len(self.textList)-1: + continue + # Codeblock flag, prevent annotation # misjudgment + if re.match(r"```", line): + code_block_flag = not code_block_flag + # Process the title + if not code_block_flag and self._getTitleLevel(line) and self._getTitleLevel(line) <= majorTitleLevel: + majorTitleLevel = self._getTitleLevel(line) + if lasti is not None: + title = re.match(self.titleLineMatchStr, + self.textList[lasti]).groups()[1] + self.SubSection.append( + MDSection(title, '\n'.join(self.textList[lasti+1:i]))) + lasti = i + if lasti is None: + self.nonSubSectionTextList.append(line) + if i == len(self.textList)-1 and lasti is not None: + title = re.match(self.titleLineMatchStr, + self.textList[lasti]).groups()[1] + self.SubSection.append( + MDSection(title, '\n'.join(self.textList[lasti+1:]))) + self.nonSubSectionText = '\n'.join(self.nonSubSectionTextList) + + def elementSplit(self, text): + """ + Split the markdown text into elements and process textline indentation. + For example: code block, equation block, multilevel-list, table(not implemented), etc. + """ + code_match = re.findall(r"(\s{0,}```.*?```)", text, re.S) + latex_match = re.findall(r"(\s{0,}\$\$.*?\$\$)", text, re.S) + table_match = re.findall(r"(\s{0,}\|.*\|)", text, re.S) + lines = text.split('\n') + outputList = [] + while lines: + # Code block + if code_match and lines and lines[0] in code_match[0]: + while lines and lines[0] in code_match[0]: + lines.pop(0) + outputList.append(code_match.pop(0)) + # Latex block + elif latex_match and lines and lines[0] in latex_match[0]: + while lines and lines[0] in latex_match[0]: + lines.pop(0) + outputList.append(latex_match.pop(0)) + # Table block + elif table_match and lines and lines[0] in table_match[0]: + while lines and lines[0] in table_match[0]: + lines.pop(0) + outputList.append(table_match.pop(0)) + elif lines: + if re.match(r'[\s\t]*$', lines[0]): # Empty line + lines.pop(0) + else: # Indent handling + line = lines.pop(0) + level = self._getListLevel(line) + if level is not None: # Note: Including the case of level 0 + topictitle = "\t"*level + \ + re.match(self.listLineMatchStr, line).groups()[2] + else: + topictitle = line + outputList.append(topictitle) + return outputList + + def toXmind(self, parentTopic, cvtEquation=False, + cvtWebImage=False, cvtHyperLink=False, + cvtTable=False, + index=-1): + """Convert the section to xmind + """ + if self.title: # For the non-Root section + topic = parentTopic.addSubTopicbyTitle(self.title) + else: # For the Root section (if title is not given, directly add the subsection to the parent topic) + topic = parentTopic + topic.addSubTopicbyIndentedList( + self.elementSplit(self.nonSubSectionText), index) + # FIXME: Maybe it is a better choice to remove these functions from TopicElement + if cvtTable: + topic.convertTitle2Table(recursive=True) + if cvtEquation: + topic.convertTitle2Equation(height=50, recursive=True) + if cvtWebImage: + topic.convertTitle2WebImage(recursive=True) + if cvtHyperLink: + topic.convertTitleWithHyperlink(recursive=True) + for subSection in self.SubSection: + subSection.toXmind(topic, cvtEquation, cvtWebImage, cvtHyperLink, cvtTable) + + def toXmindText(self, removeHyperlink=True, parentIndent=0): + """Convert the section to xmindtextlist + """ + textList = [] + if self.title: + textList.append("\t"*parentIndent + self.title) + for line in self.elementSplit(self.nonSubSectionText): + if removeHyperlink: + line = re.sub(r"\[(.*?)\]\(.*?\)", r"\1", line) + textList.append("\t"*(parentIndent+1) + line) + for subSection in self.SubSection: + textList = textList + \ + subSection.toXmindText(parentIndent=parentIndent+1) + return textList + + # Debug + def printSubSections(self, indent=4): + print(" "*indent, self.title) + for subSection in self.SubSection: + subSection.printSubSections(indent+4) + + +class MarkDown2Xmind(object): + + _ws_only_line_re = re.compile(r"^[ \t]+$", re.M) + tab_width = 4 + + def __init__(self, topic=None): + """ + Initialize MarkDown2Xmind + :param topic: Set the root topic + """ + self.topic = topic + + def _detab_line(self, line): + r"""Recusively convert tabs to spaces in a single line. + + Called from _detab().""" + if '\t' not in line: + return line + chunk1, chunk2 = line.split('\t', 1) + chunk1 += (' ' * (self.tab_width - len(chunk1) % self.tab_width)) + output = chunk1 + chunk2 + return self._detab_line(output) + + def _detab(self, text): + r"""Iterate text line by line and convert tabs to spaces. + >>> m = Markdown() + >>> m._detab("\tfoo") + ' foo' + >>> m._detab(" \tfoo") + ' foo' + >>> m._detab("\t foo") + ' foo' + >>> m._detab(" foo") + ' foo' + >>> m._detab(" foo\n\tbar\tblam") + ' foo\n bar blam' + """ + if '\t' not in text: + return text + output = [] + for line in text.splitlines(): + output.append(self._detab_line(line)) + return '\n'.join(output) + + def preProcess(self, text): + if not isinstance(text, str): + # TODO: perhaps shouldn't presume UTF-8 for string input? + text = str(text, 'utf-8') + + # Standardize line endings: + text = text.replace("\r\n", "\n") + text = text.replace("\r", "\n") + + # Make sure $text ends with a couple of newlines: + text += "\n\n" + + # Convert all tabs to spaces. + text = self._detab(text) + + # Strip any lines consisting only of spaces and tabs. + # This makes subsequent regexen easier to write, because we can + # match consecutive blank lines with /\n+/ instead of something + # contorted like /[ \t]*\n+/ . + text = self._ws_only_line_re.sub("", text) + # Remove multiple empty lines + text = re.sub(r"[\n]+", "\n", text) + return text + + def convert2xmind(self, text, cvtEquation=False, cvtWebImage=False, cvtHyperLink=False, cvtTable=False, index=-1): + """Convert the given text.""" + if not self.topic: + print("Please set the topic first") + return + text = self.preProcess(text) + mdSection = MDSection("", text) + mdSection.toXmind(self.topic, cvtEquation, + cvtWebImage, cvtHyperLink, cvtTable, index=index) + + def convert2xmindtext(self, text): + """Convert the given text.""" + buf = [] + text = self.preProcess(text) + mdSection = MDSection("", text) + textList = mdSection.toXmindText() + for item in textList: + tablevel = len(re.match(r"([\t]{0,})", item).groups()[0]) + buf.append(item.replace("\n", "\n"+"\t"*tablevel)) + # buf.append(item.replace("\n", "\r")) + return "\n".join(buf) + + def printSubSections(self, text): + """Print the sub-sections of the given text.""" + text = self.preProcess(text) + mdSection = MDSection("", text) + mdSection.printSubSections() + + +if __name__ == "__main__": + pass diff --git a/XmindCopilot/fmt_cvt/table_render.py b/XmindCopilot/fmt_cvt/table_render.py new file mode 100644 index 0000000..f953950 --- /dev/null +++ b/XmindCopilot/fmt_cvt/table_render.py @@ -0,0 +1,183 @@ +import matplotlib.pyplot as plt +import re +import unicodedata +from matplotlib import rcParams +from matplotlib.font_manager import FontProperties +from ..utils import generate_id +import tempfile +import os + +TEMP_DIR = tempfile.gettempdir() + + +def markdown_table_to_png(md_table, output_path=None, + font_family='SimHei', + font_size=12, + figsize=None, + row_height=0.3, + dpi=300): + """ + 增强版表格生成函数: + - 支持**加粗**语法渲染 + - 支持自适应布局 + - 智能列宽计算 + """ + # 配置中文字体 + rcParams['font.sans-serif'] = [font_family, 'Microsoft YaHei', 'WenQuanYi Zen Hei'] + rcParams['axes.unicode_minus'] = False + rcParams["mathtext.fontset"] = "cm" # 使用Computer Modern字体 + + if output_path is None: + output_path = os.path.join(TEMP_DIR, generate_id() + ".png") + + # 解析表格数据 + rows = [] + bold_flags = [] + for line in md_table.strip().split('\n'): + # Reserve empty cell + line = re.sub(r'\|\|', '| - |', line) + line = re.sub(r'^\||\|$', '', line) + cells = [cell.strip() for cell in line.split('|') if cell.strip()] + + # if not cells: + # continue + + # 处理加粗语法 + cleaned_cells = [] + bold_row = [] + for cell in cells: + # 清洗不可见字符 + clean_cell = re.sub(r'[\u200b-\u200d\ufeff]', '', cell) + clean_cell = ''.join(c for c in clean_cell if unicodedata.category(c)[0] != 'C') + + # 提取加粗标记 + is_bold = False + if re.search(r'\*\*.*?\*\*', clean_cell): + is_bold = True + clean_cell = re.sub(r'\*\*', '', clean_cell) + + cleaned_cells.append(clean_cell) + bold_row.append(is_bold) + if any(cell.replace('-', '') for cell in cells): + rows.append(cleaned_cells) + bold_flags.append(bold_row) + + # 验证表格结构 + if len(rows) < 2 or len(rows[0]) < 2: + raise ValueError("无效的Markdown表格格式") + + col_labels = rows[0] + cell_text = rows[1:] + col_count = len(col_labels) + + # 自动计算列宽 + font = FontProperties(family=font_family, size=font_size) + col_widths = self_adaptive_col_width(rows, font) + + # 自适应布局计算 + if figsize is None: + base_width = sum(col_widths) * 7.0 # 基础宽度系数 + figsize = ( + max(0.1, base_width), # 最小宽度4英寸 + max(0.1, (len(cell_text)+1)*row_height) # 最小高度2英寸 + ) + + # 创建图表 + fig = plt.figure(figsize=figsize) + ax = fig.add_subplot(111) + ax.axis("off") + + # 创建表格 + table = ax.table( + cellText=cell_text, + colLabels=col_labels, + loc="center", + cellLoc="center", + colWidths=col_widths, + ) + + # 应用加粗样式 + apply_bold_style(table, bold_flags, font_size) + + # 优化样式 + table.auto_set_font_size(False) + table.set_fontsize(font_size) + for key, cell in table.get_celld().items(): + cell.set_edgecolor("lightgray") + cell.set_height(row_height) # 设置固定行高(单位:英寸) + + # 紧凑布局 + plt.tight_layout(pad=0.5) + plt.savefig(output_path, dpi=dpi, bbox_inches='tight', pad_inches=0.1) + plt.close() + return output_path + + +def self_adaptive_col_width(rows, font): + """智能列宽计算算法""" + # 临时绘图对象 + temp_fig = plt.figure() + temp_ax = temp_fig.add_subplot(111) + renderer = temp_fig.canvas.get_renderer() + + # 计算每列最大宽度 + col_count = len(rows[0]) + max_widths = [0] * col_count + + for col_idx in range(col_count): + for row in rows: + text_obj = temp_ax.text(0, 0, row[col_idx], fontproperties=font) + text_obj.draw(renderer) + bbox = text_obj.get_window_extent(renderer) + max_widths[col_idx] = max(max_widths[col_idx], bbox.width) + + # 添加10%边距 + max_widths[col_idx] *= 1.2 + + # 归一化处理 + total_width = sum(max_widths) + return [w/total_width for w in max_widths] + + +def apply_bold_style(table, bold_flags, font_size): + """应用加粗样式""" + for (row, col), cell in table.get_celld().items(): + # 表头行加粗 + if row == 0: + cell.set_facecolor("#F5F5F5") + cell.set_text_props(weight='bold') + continue + + # 内容行加粗 + if row-1 < len(bold_flags) and col < len(bold_flags[row-1]): + if bold_flags[row-1][col]: + cell.set_text_props(weight='bold') + + +if __name__ == "__main__": + # 使用示例 + md_table = """ + | 维度 | RNN | LSTM | GRU | + |---------------------|-----------|------------------|-----------------| + | ​**门控数量** | 0 | 3(遗忘/输入/输出) | 2(更新/重置) | + | ​**参数数量** | 最少 | 最多(比GRU多33%) | 中等 | + | ​**计算效率** | 最高 | 最低 | 中等 | + | ​**长序列表现** | 差 | 优秀 | 良好 | + | ​**典型应用场景** | 短文本生成 $y =a^b + \\frac{1}{2}$ | 机器翻译/语音识别 | 对话系统/股票预测| + """ + md_table2 = """ + | 函数 | 公式 | 特性 | + |------------------|------------------------|------------------| + | 正弦函数 | $\sin(x) \frac{1}{2}$ | 周期为$2\pi$ | + | 欧拉公式 | $e^{i\pi} + 1 = 0$| 最美数学公式 | + """ + + md_table3 = """ + || 机制 | 作用原理 | 示例场景 | + |---|-----|---------|---------| + | 1 | 维度分解 | 每个头处理d_model/num_heads维子空间 | 512维向量用8个头时,每个头处理64维 | + | 2 | 参数独立性 | 各头的Q/K/V矩阵独立初始化训练 | 即使两个头初始关注时态,训练后可能分化出过去/未来时态处理 | + | 3 | 线性变换融合 | 最终拼接后的Wo矩阵筛选有效特征 | 重叠头的冗余信息在降维时被过滤 | + """ + + markdown_table_to_png(md_table3, "机制.png", figsize=None) diff --git a/XmindCopilot/fmt_cvt/table_render_test.py b/XmindCopilot/fmt_cvt/table_render_test.py new file mode 100644 index 0000000..a76068f --- /dev/null +++ b/XmindCopilot/fmt_cvt/table_render_test.py @@ -0,0 +1,132 @@ +import matplotlib.pyplot as plt +import re +import unicodedata +from matplotlib import rcParams +from matplotlib.font_manager import FontProperties +from matplotlib.transforms import Bbox + + +def markdown_table_to_png(md_table, output_path, + font_family='SimHei', + font_size=12, + figsize=(10, 1), + dpi=300): + """ + 修复渲染器问题的最终版本 + """ + # 配置字体和数学公式 + rcParams['font.sans-serif'] = [font_family, 'Microsoft YaHei', 'WenQuanYi Zen Hei'] + rcParams['axes.unicode_minus'] = False + rcParams['mathtext.fontset'] = 'cm' + + # 初始化字体度量 + font = FontProperties(family=font_family, size=font_size) + + # 创建临时绘图对象用于计算 + temp_fig = plt.figure(figsize=figsize) + temp_ax = temp_fig.add_subplot(111) + renderer = temp_fig.canvas.get_renderer() + + # 文本预处理函数 + def preprocess_text(text): + return re.sub(r'[\u200b-\u200d\ufeff]', '', + ''.join(c for c in text if unicodedata.category(c)[0] != 'C')) + + # 列宽计算函数(修复版) + def calculate_cell_width(text): + """使用临时轴进行渲染计算""" + text_obj = temp_ax.text(0, 0.5, text, + fontproperties=font, + math_fontfamily='cm') + text_obj.draw(renderer) + bbox = text_obj.get_window_extent(renderer) + temp_ax.cla() # 清除临时轴内容 + return bbox.width + + # 解析表格数据 + rows = [] + for line in md_table.strip().split('\n'): + line = re.sub(r'^\||\|$', '', line) + cells = [preprocess_text(cell.strip()) for cell in line.split('|')] + if any(cell.replace('-', '') for cell in cells): + rows.append(cells) + + # 验证表格结构 + if len(rows) < 2 or len(rows[0]) < 2: + raise ValueError("Invalid markdown table format") + + # 提取列标题和内容 + col_labels = rows[0] + cell_text = rows[1:] + col_count = len(col_labels) + + # 智能列宽计算 + max_widths = [0.0] * col_count + for col in range(col_count): + # 处理列标题 + header_width = calculate_cell_width(col_labels[col]) + max_widths[col] = header_width + + # 处理单元格内容 + for row in cell_text: + cell_width = calculate_cell_width(row[col]) + if cell_width > max_widths[col]: + max_widths[col] = cell_width + + # 创建最终图像 + final_fig = plt.figure(figsize=figsize) + ax = final_fig.add_subplot(111) + ax.axis("off") + + # 计算归一化列宽 + total_width = sum(max_widths) + col_widths = [w / total_width * 0.95 for w in max_widths] + + # 创建表格 + table = ax.table( + cellText=cell_text, + colLabels=col_labels, + loc="center", + cellLoc='center', + colWidths=col_widths, + ) + + # 优化样式 + table.auto_set_font_size(False) + table.set_fontsize(font_size) + for key, cell in table.get_celld().items(): + cell.set_edgecolor("lightgray") + + # 设置单元格样式 + for (row, col), cell in table.get_celld().items(): + if row == 0: + cell.set_facecolor("#F5F5F5") + cell.set_text_props(weight='bold') + if col == 0 and row > 0: + cell.set_text_props(weight='bold') + + # 保存输出 + try: + final_fig.savefig(output_path, dpi=dpi, bbox_inches='tight', pad_inches=0.1) + finally: + plt.close(temp_fig) + plt.close(final_fig) + + +# 测试用例 +# md_content = """ +# | 函数 | 公式 | 特性 | +# |------------------|------------------------|------------------| +# | 正弦函数 | $\sin(x)$ | 周期为$2\pi$ | +# | 欧拉公式 | $e^{i\pi} + 1 = 0$ | 最美数学公式 | +# """ +md_content = """ +| 维度 | RNN | LSTM | GRU | +|---------------------|-----------|------------------|-----------------| +| **门控数量** | 0 | 3(遗忘/输入/输出) | 2(更新/重置) | +| **参数数量** | 最少 $e^{i\pi} + 1 = \frac{1*a^b}{2}$ | 最多(比GRU多33%) | 中等 | +| **计算效率** | 最高 | 最低 | 中等 | +| **长序列表现** | 差 | 优秀 | 良好 | +| **典型应用场景** | 短文本生成 | 机器翻译/语音识别 | 对话系统/股票预测| +""" +markdown_table_to_png(md_content, "math_table.png") diff --git a/XmindCopilot/fmt_cvt/xmind2md.py b/XmindCopilot/fmt_cvt/xmind2md.py new file mode 100644 index 0000000..142a704 --- /dev/null +++ b/XmindCopilot/fmt_cvt/xmind2md.py @@ -0,0 +1,152 @@ +# encoding: utf-8 + +import search +import XmindCopilot +import os +from typing import Dict +import typing as typing +import sys + +import glob + + +def WalkTopic(dictXmind: Dict, resultDict: Dict): + strTitle: typing.AnyStr = dictXmind['title'] + if 'topics' in dictXmind: + pass + # print(dictXmind['topics']) + + listTopics: typing.List = dictXmind['topics'] + + if (listTopics.__len__() > 0): + resultDict[strTitle] = {} + for topic in listTopics: + WalkTopic(topic, resultDict[strTitle]) + else: + resultDict[strTitle] = strTitle + + +def Print2MDList(dictOri: typing.Dict) -> typing.AnyStr: + levelOri = 0 + listStr = [] + + def Print2MDListInternal(dictContent: typing.Dict, level): + if type(dictContent).__name__ != 'dict': + return + level = level + 1 + for topic, topicDict in dictContent.items(): + listStr.append(' ' * (level - 1)) + listStr.append('- ') + if topic: + listStr.append(topic.replace('\n', '\t')) + else: + listStr.append('*FIG*') + listStr.append('\n') + Print2MDListInternal(topicDict, level) + + Print2MDListInternal(dictOri, levelOri) + + return ''.join(listStr) + + +def xmindfiles_cvt(paths): + + for path in paths: + pathSource = path + pathSource = pathSource.replace('\\', '/') + # pathOutput = pathSource.split('/')[-1].split('.')[0] + '.xmind.md' + # 输出到原文件目录 + pathOutput = pathSource + '.md' + strResult = '' + + # 有待更新链接算法! + wikilinkpaths = glob.glob(os.path.dirname(pathSource).replace( + '\\', '/')+'/**/*.xmind', recursive=False) + for file_path in wikilinkpaths: + file_path = os.path.splitext(file_path)[0].replace('\\', '/') + file_name = file_path.split('/')[-1] + # print(file_name) + strResult += '[['+file_name+'.xmind]]\n' + + workbook = XmindCopilot.load(pathSource) + sheets = workbook.getSheets() + for sheet in sheets: + dictSheet = sheet.getData() + dictResult: Dict = {} + WalkTopic(dictSheet['topic'], dictResult) + + strResult += Print2MDList(dictResult) + + with open(pathOutput, 'w', encoding='utf-8') as f: + f.write(strResult) + print('Successfully wrote result into file: ' + pathOutput) + + +def test(): + print('sys.argv: ', sys.argv, "\n") + + pathSource = None + pathOutput = None + + for i, val in enumerate(sys.argv): + if (val == '-source'): + pathSource = sys.argv[i + 1] + if (val == '-output'): + pathOutput = sys.argv[i + 1] + + pathSource = pathSource.replace('\\', '/') + + if pathOutput == None: + # pathOutput = pathSource.split('/')[-1].split('.')[0] + '.xmind.md' + # 输出到原文件目录 + # pathOutput = pathSource.split('.xmind')[0] + '.xmind.md' + pathOutput = pathSource + '.md' + + workbook = XmindCopilot.load(pathSource) + sheet = workbook.getPrimarySheet() + dictSheet = sheet.getData() + dictResult: Dict = {} + WalkTopic(dictSheet['topic'], dictResult) + + strResult = Print2MDList(dictResult) + + with open(pathOutput, 'w', encoding='utf-8') as f: + f.write(strResult) + print('Successfully wrote result into file: ' + pathOutput) + + # print(strResult) + # print(dictSheet) + +# xmind text 2 markdown + + +def xmindtext2md(text, root_level=1, last_level=6): + """Convert xmind text to markdown text + :param text: xmind text + :param root_level: root heading level + - 0 means using multilevel list instead of heading + :param last_level: last heading level + - convert to list when level > last_level + """ + lines = text.splitlines() + ret = [] + for line in lines: + ntab = line.count('\t') + if ntab <= last_level - root_level and root_level != 0: + line = '#' * (ntab + root_level) + ' ' + line.strip() + elif root_level != 0: + line = ' ' * (ntab - last_level + root_level - 1) + \ + '- ' + line.strip() + else: + line = ' ' * (ntab-1) + '- ' + line.strip() + ret.append(line) + return '\n'.join(ret) + + +if __name__ == "__main__": + # test() + paths = search.getXmindPath() + + # paths = glob.glob('../**/*.xmind',recursive=True) + print(paths) + xmindfiles_cvt(paths) diff --git a/XmindCopilot/playerone_mgr/__init__.py b/XmindCopilot/playerone_mgr/__init__.py new file mode 100644 index 0000000..d42b98a --- /dev/null +++ b/XmindCopilot/playerone_mgr/__init__.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import XmindCopilot +from XmindCopilot.core.topic import TopicElement +from XmindCopilot.core.markerref import MarkerId +from XmindCopilot.search import topic_search +from urllib.parse import unquote +import re +import os +import copy + + +def topic_info_transfer(topic, xmind_match_str="file://(.*\.xmind8)"): + """Transfer data under xmind file topic (with suffix .xmind8) into this xmind file + + Args: + topic (_type_): father topic to traverse + xmind_match_str (str, optional): xmind file topic match string. Defaults to "file://(.*\.xmind8)". + """ + topics = topic.getSubTopics() + for t in topics: + topic_info_transfer(t) + href = topic.getHyperlink() if topic.getHyperlink() else "" + match = re.match(xmind_match_str, href) + if match and topics: + f_url = unquote(match.group(1)) + # Convert the url to utf-8 + print("Loading: ", f_url) + workbook = XmindCopilot.load(f_url) + sheets = workbook.getSheets() + if not sheets[0].getTitle(): + if os.path.isfile(f_url): + print("File doesn't exist:"+workbook.get_path()) + else: + print("Failed to open:"+workbook.get_path()) + else: + wb_root_topic = sheets[0].getRootTopic() + if not topic_search(wb_root_topic, "Draft", 1): + wb_root_topic.addSubTopicbyTitle("Draft") + Draft_topic = topic_search(wb_root_topic, "Draft", 1) + for t in topics: + # IMPORTANT When you add topic to another topic, + # the topic will be removed from the original topic automatically. + # You DO NOT have to remove it manually. + # (Because the topic instance can only have one parent topic) + Draft_topic.addSubTopic(t) + for t in Draft_topic.getSubTopics(): + print(t.getTitle()) + XmindCopilot.save(workbook) + + +def topic_info_clear(topic, xmind_match_str="file://(.*\.xmind8)"): + """clear transfered data under xmind file topic""" + topics = topic.getSubTopics() + for t in topics: + topic_info_clear(t) + href = topic.getHyperlink() if topic.getHyperlink() else "" + match = re.match(xmind_match_str, href) + if match and topics: + print("Removing subtopics of " + topic.getTitle()) + topic.removeSubTopic() diff --git a/XmindCopilot/search/__init__.py b/XmindCopilot/search/__init__.py new file mode 100644 index 0000000..a77f633 --- /dev/null +++ b/XmindCopilot/search/__init__.py @@ -0,0 +1,227 @@ +''' +Author: MasterYip 2205929492@qq.com +Date: 2023-08-20 18:06:15 +LastEditors: Raymon Yip +LastEditTime: 2025-03-10 11:04:56 +FilePath: /XmindCopilot/XmindCopilot/search/__init__.py +Description: file content +''' +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from deprecated import deprecated +import re +import os +import XmindCopilot +from ..core import const + + +class Pointer(object): + def __init__(self): + # path - list of topic titles + self.path = [] + # snapshot - record pathstr for CLI display + self.snapshot = [] + + def getpathstr(self, connectsym="->", rm_newline=True): + """获取当前路径String""" + str = "" + for p in self.path: + if rm_newline: + p = p.replace("\r\n", "") + str += p + connectsym + return str + + def printer(self): + """打印当前路径""" + print(self.getpathstr()) + + def treeprint(self): + """DEPRECATED 结构化打印当前路径 仅保留最后一项""" + if self.path: + tab = "" + for i in range(len(self.path)-1): + tab += "\t|" + print(tab+self.path[-1]) + + def snap(self, simplify=False): + """ + 记录当前路径并添加至self.snapshot + :param simplify: 是否简化路径(去除重复部分) DEPRECATED + """ + if simplify: # DEPRECATED + result = "" + path = self.getpathstr() + if self.snapshot: + priouspath = self.snapshot[-1] + flag = 1 + for i in range(len(path)): + if i < len(priouspath): + if path[i] == priouspath[i] and flag: + # result += " " + pass + else: + flag = 0 + result += path[i] + else: + result += path[i] + print(result) + self.snapshot.append(result) + else: + self.snapshot.append(self.getpathstr()) + + +""" Title_search """ + + +def topic_search(topic, str, depth: int = -1, re_match=False): + """ + Search for title containing str (return fisrt topic matched) + """ + title = topic.getTitle() + # print(title,'\n') + if title and (re_match and re.search(str, title) or str in title): + return topic + + subtopiclist = topic.getSubTopics() + if depth == -1 and subtopiclist: + for t in subtopiclist: + if topic_search(t, str): + return topic_search(t, str) + elif depth > 0 and subtopiclist: + for t in subtopiclist: + if topic_search(t, str, depth=depth-1): + return topic_search(t, str, depth=depth-1) + + return None + +def topic_search_by_title(topic, title, depth: int = -1): + # Search for title containing str(return fisrt topic matched) + title_ = topic.getTitle() + # print(title,'\n') + if title and (title == title_): + return topic + + subtopiclist = topic.getSubTopics() + if depth == -1 and subtopiclist: + for t in subtopiclist: + if topic_search_by_title(t, title): + return topic_search_by_title(t, title) + elif depth > 0 and subtopiclist: + for t in subtopiclist: + if topic_search_by_title(t, title, depth=depth-1): + return topic_search_by_title(t, title, depth=depth-1) + + return None + +def topic_search_by_hyperlink(topic, url, depth: int = -1): + # Search for title(return fisrt topic matched) + hyperlink = topic.getHyperlink() + + if hyperlink and (url == hyperlink): + return topic + + subtopiclist = topic.getSubTopics() + if depth == -1 and subtopiclist: + for t in subtopiclist: + if topic_search_by_hyperlink(t, url): + return topic_search_by_hyperlink(t, url) + elif depth > 0 and subtopiclist: + for t in subtopiclist: + if topic_search_by_hyperlink(t, url, depth=depth-1): + return topic_search_by_hyperlink(t, url, depth=depth-1) + + return None + +def topic_search_snap(topic, ptr, str): + title = topic.getTitle() + if title: + ptr.path.append(title) + # 是否包含在标题中(正则表达式) + if re.search(str, title): + ptr.snap() + # ptr.treeprint() + # 并没有节省时间? + # ptr.path.pop() + # return + else: + ptr.path.append("[Title Empty]") + + subtopiclist = topic.getSubTopics() + if subtopiclist: + for t in subtopiclist: + topic_search_snap(t, ptr, str) + ptr.path.pop() + return + + +@deprecated("Not used anymore") +def getTopicAddress(topic): + """ + 获取目标topic在workbook中的路径(停用) + """ + connectsym = "->" + route = "" + parent = topic + type = parent.getType() + while type != const.TOPIC_ROOT: + title = parent.getTitle() + if title: + route = title + connectsym + route + else: + route = "#FIG#" + connectsym + route + parent = parent.getParentTopic() + type = parent.getType() + title = parent.getTitle() + route = title + connectsym + route + return route + + +""" Xmind File Search """ + + +def workbooksearch(path, str): + workbook = XmindCopilot.load(path, get_refs=False) + sheets = workbook.getSheets() + search_result = [] + if sheets[0].getTitle(): + for sheet in sheets: + root_topic = sheet.getRootTopic() + ptr = Pointer() + # FIXME: 目前此函数只能从roottopic开始 + topic_search_snap(root_topic, ptr, str) + search_result += ptr.snapshot + else: + if os.path.isfile(path): + print("Failed to open:"+workbook.get_path()) + else: + print("File doesn't exist:"+workbook.get_path()) + return search_result + + +""" Batch Search """ + + +def BatchSearch(searchstr, paths, verbose=True): + """ + Batch Search for xmind files + :param searchstr: search string + :param paths: xmind file path list + :param verbose: whether to print the search result + """ + tot_result = {} + for path in paths: + search_result = workbooksearch(path, searchstr) + if search_result: + tot_result[path] = search_result + if verbose: + print("\033[92m"+path+"\033[0m") + for r in search_result: + # r = r.replace("\n", " ") + r = r.replace( + searchstr, "\033[1;91m"+searchstr+"\033[1;0m") + r = r.replace("->", "\033[1;96m->\033[1;0m") + print(r) + print("\n") + + return tot_result diff --git a/XmindCopilot/topic_cluster/TextCluster/.gitignore b/XmindCopilot/topic_cluster/TextCluster/.gitignore new file mode 100644 index 0000000..28b9115 --- /dev/null +++ b/XmindCopilot/topic_cluster/TextCluster/.gitignore @@ -0,0 +1,3 @@ +*.pyc +.DS_Store +/data/output diff --git a/XmindCopilot/topic_cluster/TextCluster/LICENSE b/XmindCopilot/topic_cluster/TextCluster/LICENSE new file mode 100644 index 0000000..403c038 --- /dev/null +++ b/XmindCopilot/topic_cluster/TextCluster/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2019, Randy Pen +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the author nor the names of its contributors may be used + to endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/XmindCopilot/topic_cluster/TextCluster/README.md b/XmindCopilot/topic_cluster/TextCluster/README.md new file mode 100644 index 0000000..673d9a4 --- /dev/null +++ b/XmindCopilot/topic_cluster/TextCluster/README.md @@ -0,0 +1,118 @@ +# 短文本聚类 + +### 项目介绍 +短文本聚类是常用的文本预处理步骤,可以用于洞察文本常见模式、分析设计语义解析规范、加速相似句子查询等。本项目实现了内存友好的短文本聚类方法,并提供了相似句子查询接口。 + + + +### 依赖库 + +> pip install tqdm jieba + + + +### 使用方法 +#### 聚类 +```bash +python cluster.py --infile ./data/infile \ +--output ./data/output +``` +具体参数设置可以参考```cluster.py```文件内```_get_parser()```函数参数说明,包含设置分词词典、停用词、匹配采样数、匹配度阈值等。 +#### 查询 +参考```search.py```代码里```Searcher```类的使用方法,如果用于查询标注数据的场景,使用分隔符```:::```将句子与标注信息拼接起来。如```我是海贼王:::(λx.海贼王)```,处理时会只对句子进行匹配。 + + +### 算法原理 + +![算法原理](./data/images/Algorithm_cn.png) + + + +### 文件路径 + +```html +TextCluster +| README.md +| LICENSE +| cluster.py 聚类程序 +| search.py 查询程序 +| +|------utils 公共功能模块 +| | __init__.py +| | segmentor.py 分词器封装 +| | similar.py 相似度计算函数 +| | utils.py 文件处理模块 +| +|------data +| | infile 默认输入文本路径,用于测试中文模式 +| | infile_en 默认输入文本路径,用于测试英文模式 +| | seg_dict 默认分词词典 +| | stop_words 默认停用词路径 +``` + + + +注:本方法仅面向短文本,长文本聚类可根据需求选用[SimHash](https://en.wikipedia.org/wiki/SimHash), [LDA](https://en.wikipedia.org/wiki/Latent_Dirichlet_allocation)等其他算法。 + + + +# Text Cluster + +### Introduction + +Text cluster is a normal preprocess procedure to analysis text feature. This project implements a memory friendly method only for **short text cluster**. For long text, it is preferable to choose [SimHash](https://en.wikipedia.org/wiki/SimHash) or [LDA](https://en.wikipedia.org/wiki/Latent_Dirichlet_allocation) or others according to demand. + + + +### Requirements + +> pip install tqdm spacy + + + +### Usage +#### Clustering +```bash +python cluster.py --infile ./data/infile_en \ +--output ./data/output \ +--lang en +``` +For more configure arguments description, see ```_get_parser()``` in ```cluster.py```, including stop words setting, sample number. +#### Search + + + +### Basic Idea + +![Algorithm_en](./data/images/Algorithm_en.png) + +### File Structure + +```html +TextCluster +| README.md +| LICENSE +| cluster.py clustering function +| search.py search function +| +|------utils utilities +| | __init__.py +| | segmentor.py tokenizer wrapper +| | similar.py similarity calculator +| | utils.py file process module +| +|------data +| | infile default input file path, to test Chinese mode +| | infile_en default input file path, to test English mode +| | seg_dict default tokenizer dict path +| | stop_words default stop words path +``` + + + + + +# Other Language + +For other specific language, modify tokenizer wrapper in ```./utils/segmentor.py```. + diff --git a/XmindCopilot/topic_cluster/TextCluster/__init__.py b/XmindCopilot/topic_cluster/TextCluster/__init__.py new file mode 100644 index 0000000..d6bd6f6 --- /dev/null +++ b/XmindCopilot/topic_cluster/TextCluster/__init__.py @@ -0,0 +1,3 @@ +# from .utils.similar import jaccard +# from .utils.segmentor import Segmentor +# from .utils.utils import check_file, ensure_dir, clean_dir, sample_file, get_stop_words, line_counter, Range \ No newline at end of file diff --git a/XmindCopilot/topic_cluster/TextCluster/cluster.py b/XmindCopilot/topic_cluster/TextCluster/cluster.py new file mode 100644 index 0000000..f8204bd --- /dev/null +++ b/XmindCopilot/topic_cluster/TextCluster/cluster.py @@ -0,0 +1,199 @@ +# -*- coding: utf-8 -*- +from ast import arg +import os +import argparse +import pickle +import re +from typing import Optional +from collections import defaultdict + +from tqdm import tqdm + +from .utils.similar import jaccard +from .utils.segmentor import Segmentor +from .utils.utils import check_file, ensure_dir, clean_dir, sample_file, get_stop_words, line_counter, Range + +import argparse + +# Directory Management +try: + # Run in Terminal + ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) +except Warning: + # Run in ipykernel & interactive + ROOT_DIR = os.getcwd() + +# Constants +DEFAULT_INFILE = os.path.join(ROOT_DIR, 'data/infile') +DEFAULT_OUTPUT = os.path.join(ROOT_DIR, 'data/output') +DEFAULT_DICT = os.path.join(ROOT_DIR, 'data/seg_dict') +DEFAULT_STOP_WORDS = os.path.join(ROOT_DIR, 'data/stop_words') +DEFAULT_SAMPLE_NUMBER = 5 +DEFAULT_THRESHOLD = 0.04 # Default 0.3 +DEFAULT_NAME_LEN = 4 +DEFAULT_NAME_LEN_UPDATE = False +DEFAULT_LANG = 'cn' + + +class ClusterArgs(object): + def __init__(self): + self.infile = DEFAULT_INFILE + self.output = DEFAULT_OUTPUT + self.dict = DEFAULT_DICT + self.stop_words = DEFAULT_STOP_WORDS + self.sample_number = DEFAULT_SAMPLE_NUMBER + self.threshold = DEFAULT_THRESHOLD + self.name_len = DEFAULT_NAME_LEN + self.name_len_update = DEFAULT_NAME_LEN_UPDATE + self.lang = DEFAULT_LANG + + +def _get_parser(): + parser = argparse.ArgumentParser() + parser.add_argument('--infile', type=str, default=DEFAULT_INFILE, help='Directory of input file.') + parser.add_argument('--output', type=str, default=DEFAULT_OUTPUT, help='Directory to save output file.') + parser.add_argument('--dict', type=str, default=DEFAULT_DICT, help='Directory of dict file.') + parser.add_argument('--stop_words', type=str, default=DEFAULT_STOP_WORDS, help='Directory of stop words.') + parser.add_argument('--sample_number', type=int, default=DEFAULT_SAMPLE_NUMBER, choices=range(1), help='Sample number for each bucket.') + parser.add_argument('--threshold', type=float, default=DEFAULT_THRESHOLD, choices=range(0.0, 1.0), help='Threshold for matching.') + parser.add_argument('--name_len', type=int, default=DEFAULT_NAME_LEN, choices=range(2), help='Filename length.') + parser.add_argument('--name_len_update', type=bool, default=DEFAULT_NAME_LEN_UPDATE, help='To update file name length.') + parser.add_argument('--lang', type=str, choices=['cn', 'en'], default=DEFAULT_LANG, help='Segmentor language setting.') + args = parser.parse_args() + return args + + +def cluster(args, input: Optional[list] = None, ret_output=True): + ''' + description: + param {*} args - origin args + param {*} input - If input is not None, then args.infile will be ignored. + param {*} ret_output - If ret_output is True, then return output list. + return {*} + ''' + # preliminary work + check_file(args.infile) + ensure_dir(args.output) + + if args.name_len_update: + line_cnt = line_counter(args.infile) + args.name_len = len(str(line_cnt)) + 1 + + clean_dir(args.output, args.name_len) + # end preliminary work + + p_bucket = defaultdict(list) + save_idx = 0 + id_name = '{0:0' + str(args.name_len) + 'd}' + # load stop words + stop_words = get_stop_words(args.stop_words) if os.path.exists(args.stop_words) else list() + # load tokenizer + seg = Segmentor(args) + + # print('Splitting sentence into different clusters ...') + if input: + data = input + else: + infile = open(args.infile, 'r', encoding="utf-8") + data = tqdm(infile) + + for inline in data: + # Skip empty line + if not inline: + continue + inline = inline.rstrip() + line = inline.split(':::')[0] + is_match = False + seg_list = list(seg.cut(line)) + if stop_words: + seg_list = list(filter(lambda x: x not in stop_words, seg_list)) + for wd in seg_list: + if is_match: + break + w_bucket = p_bucket[wd] + for bucket in w_bucket: + bucket_path = os.path.join(args.output, bucket) + check_file(bucket_path) + selected = sample_file(bucket_path, args.sample_number) + selected = list(map(lambda x: x.split(':::')[0], selected)) + selected = list(map(lambda x: list(seg.cut(x)), selected)) + # remove stop words + if stop_words: + filt_selected = list() + for sen in selected: + sen = list(filter(lambda x: x not in stop_words, sen)) + filt_selected.append(sen) + selected = filt_selected + # calculate similarity with each bucket + if all(jaccard(seg_list, cmp_list) > args.threshold for cmp_list in selected): + is_match = True + with open(bucket_path, 'a', encoding='utf-8') as outfile: + outfile.write(line+'\n') + for w in seg_list: + if bucket not in p_bucket[w]: + p_bucket[w].append(bucket) + break + if not is_match: + bucket_name = ('tmp' + id_name).format(save_idx) + bucket_path = os.path.join(args.output, bucket_name) + with open(bucket_path, 'a', encoding='utf-8') as outfile: + outfile.write(line+'\n') + for w in seg_list: + p_bucket[w].append(bucket_name) + save_idx += 1 + if not input: + infile.close() + + # sort and rename file + file_list = os.listdir(args.output) + file_list = list(filter(lambda x: x.startswith('tmp'), file_list)) + cnt = dict() + for file in file_list: + file_path = os.path.join(args.output, file) + cnt[file] = line_counter(file_path) + + sorted_cnt = sorted(cnt.items(), key=lambda kv: kv[1], reverse=True) + name_map = dict() + for idx, (file_name, times) in enumerate(sorted_cnt): + origin_path = os.path.join(args.output, file_name) + new_name = id_name.format(idx) + new_path = os.path.join(args.output, new_name) + os.rename(origin_path, new_path) + name_map[file_name] = new_name + + for k, v in p_bucket.items(): + p_bucket[k] = list(map(lambda x: name_map[x], v)) + + p_bucket_path = os.path.join(args.output, 'p_bucket.pickle') + with open(p_bucket_path, 'wb') as outfile: + pickle.dump(p_bucket, outfile, protocol=pickle.HIGHEST_PROTOCOL) + + # print('All is well') + if ret_output: + return get_clustered_data() + + +def get_clustered_data(): + data = [] + for root, folder, file in os.walk(DEFAULT_OUTPUT): + for f in file: + if re.match(r'^\d+$', f): + file_data = [] + file_path = os.path.join(root, f) + with open(file_path, 'r', encoding='utf-8') as infile: + for line in infile: + file_data.append(line.rstrip()) + data.append(file_data) + return data + +def main(): + args = _get_parser() + cluster(args) + +if __name__ == '__main__': + # Python + args = ClusterArgs() + cluster(args) + + # CLI + # main() diff --git a/XmindCopilot/topic_cluster/TextCluster/data/infile b/XmindCopilot/topic_cluster/TextCluster/data/infile new file mode 100644 index 0000000..e69de29 diff --git a/XmindCopilot/topic_cluster/TextCluster/data/infile_en b/XmindCopilot/topic_cluster/TextCluster/data/infile_en new file mode 100644 index 0000000..4032aeb --- /dev/null +++ b/XmindCopilot/topic_cluster/TextCluster/data/infile_en @@ -0,0 +1,12 @@ +Thank you +Are you ok +Hello +Thank you +Thank you very much +Hello +Thank you +Thank you very much +He +He hello +Thank you +Thank you very much diff --git a/XmindCopilot/topic_cluster/TextCluster/data/seg_dict b/XmindCopilot/topic_cluster/TextCluster/data/seg_dict new file mode 100644 index 0000000..5254cab --- /dev/null +++ b/XmindCopilot/topic_cluster/TextCluster/data/seg_dict @@ -0,0 +1 @@ +李小龙 5 nr diff --git a/XmindCopilot/topic_cluster/TextCluster/data/stop_words b/XmindCopilot/topic_cluster/TextCluster/data/stop_words new file mode 100644 index 0000000..e76f729 --- /dev/null +++ b/XmindCopilot/topic_cluster/TextCluster/data/stop_words @@ -0,0 +1,1895 @@ +回答 +知乎 +! +" +# +$ +% +& +' +( +) +* ++ +, +- +-- +. +.. +... +...... +................... +./ +.一 +.数 +.日 +/ +// +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +: +:// +:: +; +< += +> +>> +? +@ +A +Lex +[ +\ +] +^ +_ +` +exp +sub +sup +| +} +~ +~~~~ +· +× +××× +Δ +Ψ +γ +μ +φ +φ. +В +— +—— +——— +‘ +’ +’‘ +“ +” +”, +… +…… +…………………………………………………③ +′∈ +′| +℃ +Ⅲ +↑ +→ +∈[ +∪φ∈ +≈ +① +② +②c +③ +③] +④ +⑤ +⑥ +⑦ +⑧ +⑨ +⑩ +── +■ +▲ +  +、 +。 +〈 +〉 +《 +》 +》), +」 +『 +』 +【 +】 +〔 +〕 +〕〔 +㈧ +一 +一. +一一 +一下 +一个 +一些 +一何 +一切 +一则 +一则通过 +一天 +一定 +一方面 +一旦 +一时 +一来 +一样 +一次 +一片 +一番 +一直 +一致 +一般 +一起 +一转眼 +一边 +一面 +七 +万一 +三 +三天两头 +三番两次 +三番五次 +上 +上下 +上升 +上去 +上来 +上述 +上面 +下 +下列 +下去 +下来 +下面 +不 +不一 +不下 +不久 +不了 +不亦乐乎 +不仅 +不仅...而且 +不仅仅 +不仅仅是 +不会 +不但 +不但...而且 +不光 +不免 +不再 +不力 +不单 +不变 +不只 +不可 +不可开交 +不可抗拒 +不同 +不外 +不外乎 +不够 +不大 +不如 +不妨 +不定 +不对 +不少 +不尽 +不尽然 +不巧 +不已 +不常 +不得 +不得不 +不得了 +不得已 +不必 +不怎么 +不怕 +不惟 +不成 +不拘 +不择手段 +不敢 +不料 +不断 +不日 +不时 +不是 +不曾 +不止 +不止一次 +不比 +不消 +不满 +不然 +不然的话 +不特 +不独 +不由得 +不知不觉 +不管 +不管怎样 +不经意 +不胜 +不能 +不能不 +不至于 +不若 +不要 +不论 +不起 +不足 +不过 +不迭 +不问 +不限 +与 +与其 +与其说 +与否 +与此同时 +专门 +且 +且不说 +且说 +两者 +严格 +严重 +个 +个人 +个别 +中小 +中间 +丰富 +串行 +临 +临到 +为 +为主 +为了 +为什么 +为什麽 +为何 +为止 +为此 +为着 +主张 +主要 +举凡 +举行 +乃 +乃至 +乃至于 +么 +之 +之一 +之前 +之后 +之後 +之所以 +之类 +乌乎 +乎 +乒 +乘 +乘势 +乘机 +乘胜 +乘虚 +乘隙 +九 +也 +也好 +也就是说 +也是 +也罢 +了 +了解 +争取 +二 +二来 +二话不说 +二话没说 +于 +于是 +于是乎 +云云 +云尔 +互 +互相 +五 +些 +交口 +亦 +产生 +亲口 +亲手 +亲眼 +亲自 +亲身 +人 +人人 +人们 +人家 +人民 +什么 +什么样 +什麽 +仅 +仅仅 +今 +今后 +今天 +今年 +今後 +介于 +仍 +仍旧 +仍然 +从 +从不 +从严 +从中 +从事 +从今以后 +从优 +从古到今 +从古至今 +从头 +从宽 +从小 +从新 +从无到有 +从早到晚 +从未 +从来 +从此 +从此以后 +从而 +从轻 +从速 +从重 +他 +他人 +他们 +他是 +他的 +代替 +以 +以上 +以下 +以为 +以便 +以免 +以前 +以及 +以后 +以外 +以後 +以故 +以期 +以来 +以至 +以至于 +以致 +们 +任 +任何 +任凭 +任务 +企图 +伙同 +会 +伟大 +传 +传说 +传闻 +似乎 +似的 +但 +但凡 +但愿 +但是 +何 +何乐而不为 +何以 +何况 +何处 +何妨 +何尝 +何必 +何时 +何止 +何苦 +何须 +余外 +作为 +你 +你们 +你是 +你的 +使 +使得 +使用 +例如 +依 +依据 +依照 +依靠 +便 +便于 +促进 +保持 +保管 +保险 +俺 +俺们 +倍加 +倍感 +倒不如 +倒不如说 +倒是 +倘 +倘使 +倘或 +倘然 +倘若 +借 +借以 +借此 +假使 +假如 +假若 +偏偏 +做到 +偶尔 +偶而 +傥然 +像 +儿 +允许 +元/吨 +充其极 +充其量 +充分 +先不先 +先后 +先後 +先生 +光 +光是 +全体 +全力 +全年 +全然 +全身心 +全部 +全都 +全面 +八 +八成 +公然 +六 +兮 +共 +共同 +共总 +关于 +其 +其一 +其中 +其二 +其他 +其余 +其后 +其它 +其实 +其次 +具体 +具体地说 +具体来说 +具体说来 +具有 +兼之 +内 +再 +再其次 +再则 +再有 +再次 +再者 +再者说 +再说 +冒 +冲 +决不 +决定 +决非 +况且 +准备 +凑巧 +凝神 +几 +几乎 +几度 +几时 +几番 +几经 +凡 +凡是 +凭 +凭借 +出 +出于 +出去 +出来 +出现 +分别 +分头 +分期 +分期分批 +切 +切不可 +切切 +切勿 +切莫 +则 +则甚 +刚 +刚好 +刚巧 +刚才 +初 +别 +别人 +别处 +别是 +别的 +别管 +别说 +到 +到了儿 +到处 +到头 +到头来 +到底 +到目前为止 +前后 +前此 +前者 +前进 +前面 +加上 +加之 +加以 +加入 +加强 +动不动 +动辄 +勃然 +匆匆 +十分 +千 +千万 +千万千万 +半 +单 +单单 +单纯 +即 +即令 +即使 +即便 +即刻 +即如 +即将 +即或 +即是说 +即若 +却 +却不 +历 +原来 +去 +又 +又及 +及 +及其 +及时 +及至 +双方 +反之 +反之亦然 +反之则 +反倒 +反倒是 +反应 +反手 +反映 +反而 +反过来 +反过来说 +取得 +取道 +受到 +变成 +古来 +另 +另一个 +另一方面 +另外 +另悉 +另方面 +另行 +只 +只当 +只怕 +只是 +只有 +只消 +只要 +只限 +叫 +叫做 +召开 +叮咚 +叮当 +可 +可以 +可好 +可是 +可能 +可见 +各 +各个 +各人 +各位 +各地 +各式 +各种 +各级 +各自 +合理 +同 +同一 +同时 +同样 +后 +后来 +后者 +后面 +向 +向使 +向着 +吓 +吗 +否则 +吧 +吧哒 +吱 +呀 +呃 +呆呆地 +呐 +呕 +呗 +呜 +呜呼 +呢 +周围 +呵 +呵呵 +呸 +呼哧 +呼啦 +咋 +和 +咚 +咦 +咧 +咱 +咱们 +咳 +哇 +哈 +哈哈 +哉 +哎 +哎呀 +哎哟 +哗 +哗啦 +哟 +哦 +哩 +哪 +哪个 +哪些 +哪儿 +哪天 +哪年 +哪怕 +哪样 +哪边 +哪里 +哼 +哼唷 +唉 +唯有 +啊 +啊呀 +啊哈 +啊哟 +啐 +啥 +啦 +啪达 +啷当 +喀 +喂 +喏 +喔唷 +喽 +嗡 +嗡嗡 +嗬 +嗯 +嗳 +嘎 +嘎嘎 +嘎登 +嘘 +嘛 +嘻 +嘿 +嘿嘿 +四 +因 +因为 +因了 +因此 +因着 +因而 +固 +固然 +在 +在下 +在于 +地 +均 +坚决 +坚持 +基于 +基本 +基本上 +处在 +处处 +处理 +复杂 +多 +多么 +多亏 +多多 +多多少少 +多多益善 +多少 +多年前 +多年来 +多数 +多次 +够瞧的 +大 +大不了 +大举 +大事 +大体 +大体上 +大凡 +大力 +大多 +大多数 +大大 +大家 +大张旗鼓 +大批 +大抵 +大概 +大略 +大约 +大致 +大都 +大量 +大面儿上 +失去 +奇 +奈 +奋勇 +她 +她们 +她是 +她的 +好 +好在 +好的 +好象 +如 +如上 +如上所述 +如下 +如今 +如何 +如其 +如前所述 +如同 +如常 +如是 +如期 +如果 +如次 +如此 +如此等等 +如若 +始而 +姑且 +存在 +存心 +孰料 +孰知 +宁 +宁可 +宁愿 +宁肯 +它 +它们 +它们的 +它是 +它的 +安全 +完全 +完成 +定 +实现 +实际 +宣布 +容易 +密切 +对 +对于 +对应 +对待 +对方 +对比 +将 +将才 +将要 +将近 +小 +少数 +尔 +尔后 +尔尔 +尔等 +尚且 +尤其 +就 +就地 +就是 +就是了 +就是说 +就此 +就算 +就要 +尽 +尽可能 +尽如人意 +尽心尽力 +尽心竭力 +尽快 +尽早 +尽然 +尽管 +尽管如此 +尽量 +局外 +居然 +届时 +属于 +屡 +屡屡 +屡次 +屡次三番 +岂 +岂但 +岂止 +岂非 +川流不息 +左右 +巨大 +巩固 +差一点 +差不多 +己 +已 +已矣 +已经 +巴 +巴巴 +带 +帮助 +常 +常常 +常言说 +常言说得好 +常言道 +平素 +年复一年 +并 +并不 +并不是 +并且 +并排 +并无 +并没 +并没有 +并肩 +并非 +广大 +广泛 +应当 +应用 +应该 +庶乎 +庶几 +开外 +开始 +开展 +引起 +弗 +弹指之间 +强烈 +强调 +归 +归根到底 +归根结底 +归齐 +当 +当下 +当中 +当儿 +当前 +当即 +当口儿 +当地 +当场 +当头 +当庭 +当时 +当然 +当真 +当着 +形成 +彻夜 +彻底 +彼 +彼时 +彼此 +往 +往往 +待 +待到 +很 +很多 +很少 +後来 +後面 +得 +得了 +得出 +得到 +得天独厚 +得起 +心里 +必 +必定 +必将 +必然 +必要 +必须 +快 +快要 +忽地 +忽然 +怎 +怎么 +怎么办 +怎么样 +怎奈 +怎样 +怎麽 +怕 +急匆匆 +怪 +怪不得 +总之 +总是 +总的来看 +总的来说 +总的说来 +总结 +总而言之 +恍然 +恐怕 +恰似 +恰好 +恰如 +恰巧 +恰恰 +恰恰相反 +恰逢 +您 +您们 +您是 +惟其 +惯常 +意思 +愤然 +愿意 +慢说 +成为 +成年 +成年累月 +成心 +我 +我们 +我是 +我的 +或 +或则 +或多或少 +或是 +或曰 +或者 +或许 +战斗 +截然 +截至 +所 +所以 +所在 +所幸 +所有 +所谓 +才 +才能 +扑通 +打 +打从 +打开天窗说亮话 +扩大 +把 +抑或 +抽冷子 +拦腰 +拿 +按 +按时 +按期 +按照 +按理 +按说 +挨个 +挨家挨户 +挨次 +挨着 +挨门挨户 +挨门逐户 +换句话说 +换言之 +据 +据实 +据悉 +据我所知 +据此 +据称 +据说 +掌握 +接下来 +接着 +接著 +接连不断 +放量 +故 +故意 +故此 +故而 +敞开儿 +敢 +敢于 +敢情 +数/ +整个 +断然 +方 +方便 +方才 +方能 +方面 +旁人 +无 +无宁 +无法 +无论 +既 +既...又 +既往 +既是 +既然 +日复一日 +日渐 +日益 +日臻 +日见 +时候 +昂然 +明显 +明确 +是 +是不是 +是以 +是否 +是的 +显然 +显著 +普通 +普遍 +暗中 +暗地里 +暗自 +更 +更为 +更加 +更进一步 +曾 +曾经 +替 +替代 +最 +最后 +最大 +最好 +最後 +最近 +最高 +有 +有些 +有关 +有利 +有力 +有及 +有所 +有效 +有时 +有点 +有的 +有的是 +有着 +有著 +望 +朝 +朝着 +末##末 +本 +本人 +本地 +本着 +本身 +权时 +来 +来不及 +来得及 +来看 +来着 +来自 +来讲 +来说 +极 +极为 +极了 +极其 +极力 +极大 +极度 +极端 +构成 +果然 +果真 +某 +某个 +某些 +某某 +根据 +根本 +格外 +梆 +概 +次第 +欢迎 +欤 +正值 +正在 +正如 +正巧 +正常 +正是 +此 +此中 +此后 +此地 +此处 +此外 +此时 +此次 +此间 +殆 +毋宁 +每 +每个 +每天 +每年 +每当 +每时每刻 +每每 +每逢 +比 +比及 +比如 +比如说 +比方 +比照 +比起 +比较 +毕竟 +毫不 +毫无 +毫无例外 +毫无保留地 +汝 +沙沙 +没 +没奈何 +没有 +沿 +沿着 +注意 +活 +深入 +清楚 +满 +满足 +漫说 +焉 +然 +然则 +然后 +然後 +然而 +照 +照着 +牢牢 +特别是 +特殊 +特点 +犹且 +犹自 +独 +独自 +猛然 +猛然间 +率尔 +率然 +现代 +现在 +理应 +理当 +理该 +瑟瑟 +甚且 +甚么 +甚或 +甚而 +甚至 +甚至于 +用 +用来 +甫 +甭 +由 +由于 +由是 +由此 +由此可见 +略 +略为 +略加 +略微 +白 +白白 +的 +的确 +的话 +皆可 +目前 +直到 +直接 +相似 +相信 +相反 +相同 +相对 +相对而言 +相应 +相当 +相等 +省得 +看 +看上去 +看出 +看到 +看来 +看样子 +看看 +看见 +看起来 +真是 +真正 +眨眼 +着 +着呢 +矣 +矣乎 +矣哉 +知道 +砰 +确定 +碰巧 +社会主义 +离 +种 +积极 +移动 +究竟 +穷年累月 +突出 +突然 +窃 +立 +立刻 +立即 +立地 +立时 +立马 +竟 +竟然 +竟而 +第 +第二 +等 +等到 +等等 +策略地 +简直 +简而言之 +简言之 +管 +类如 +粗 +精光 +紧接着 +累年 +累次 +纯 +纯粹 +纵 +纵令 +纵使 +纵然 +练习 +组成 +经 +经常 +经过 +结合 +结果 +给 +绝 +绝不 +绝对 +绝非 +绝顶 +继之 +继后 +继续 +继而 +维持 +综上所述 +缕缕 +罢了 +老 +老大 +老是 +老老实实 +考虑 +者 +而 +而且 +而况 +而又 +而后 +而外 +而已 +而是 +而言 +而论 +联系 +联袂 +背地里 +背靠背 +能 +能否 +能够 +腾 +自 +自个儿 +自从 +自各儿 +自后 +自家 +自己 +自打 +自身 +臭 +至 +至于 +至今 +至若 +致 +般的 +良好 +若 +若夫 +若是 +若果 +若非 +范围 +莫 +莫不 +莫不然 +莫如 +莫若 +莫非 +获得 +藉以 +虽 +虽则 +虽然 +虽说 +蛮 +行为 +行动 +表明 +表示 +被 +要 +要不 +要不是 +要不然 +要么 +要是 +要求 +见 +规定 +觉得 +譬喻 +譬如 +认为 +认真 +认识 +让 +许多 +论 +论说 +设使 +设或 +设若 +诚如 +诚然 +话说 +该 +该当 +说明 +说来 +说说 +请勿 +诸 +诸位 +诸如 +谁 +谁人 +谁料 +谁知 +谨 +豁然 +贼死 +赖以 +赶 +赶快 +赶早不赶晚 +起 +起先 +起初 +起头 +起来 +起见 +起首 +趁 +趁便 +趁势 +趁早 +趁机 +趁热 +趁着 +越是 +距 +跟 +路经 +转动 +转变 +转贴 +轰然 +较 +较为 +较之 +较比 +边 +达到 +达旦 +迄 +迅速 +过 +过于 +过去 +过来 +运用 +近 +近几年来 +近年来 +近来 +还 +还是 +还有 +还要 +这 +这一来 +这个 +这么 +这么些 +这么样 +这么点儿 +这些 +这会儿 +这儿 +这就是说 +这时 +这样 +这次 +这点 +这种 +这般 +这边 +这里 +这麽 +进入 +进去 +进来 +进步 +进而 +进行 +连 +连同 +连声 +连日 +连日来 +连袂 +连连 +迟早 +迫于 +适应 +适当 +适用 +逐步 +逐渐 +通常 +通过 +造成 +逢 +遇到 +遭到 +遵循 +遵照 +避免 +那 +那个 +那么 +那么些 +那么样 +那些 +那会儿 +那儿 +那时 +那末 +那样 +那般 +那边 +那里 +那麽 +部分 +都 +鄙人 +采取 +里面 +重大 +重新 +重要 +鉴于 +针对 +长期以来 +长此下去 +长线 +长话短说 +问题 +间或 +防止 +阿 +附近 +陈年 +限制 +陡然 +除 +除了 +除却 +除去 +除外 +除开 +除此 +除此之外 +除此以外 +除此而外 +除非 +随 +随后 +随时 +随着 +随著 +隔夜 +隔日 +难得 +难怪 +难说 +难道 +难道说 +集中 +零 +需要 +非但 +非常 +非徒 +非得 +非特 +非独 +靠 +顶多 +顷 +顷刻 +顷刻之间 +顷刻间 +顺 +顺着 +顿时 +颇 +风雨无阻 +饱 +首先 +马上 +高低 +高兴 +默然 +默默地 +齐 +︿ +! +# +$ +% +& +' +( +) +)÷(1- +)、 +* ++ ++ξ +++ +, +,也 +- +-β +-- +-[*]- +. +/ +0 +0:2 +1 +1. +12% +2 +2.3% +3 +4 +5 +5:0 +6 +7 +8 +9 +: +; +< +<± +<Δ +<λ +<φ +<< += +=″ +=☆ +=( +=- +=[ +={ +> +>λ +? +@ +A +LI +R.L. +ZXFITL +[ +[①①] +[①②] +[①③] +[①④] +[①⑤] +[①⑥] +[①⑦] +[①⑧] +[①⑨] +[①A] +[①B] +[①C] +[①D] +[①E] +[①] +[①a] +[①c] +[①d] +[①e] +[①f] +[①g] +[①h] +[①i] +[①o] +[② +[②①] +[②②] +[②③] +[②④ +[②⑤] +[②⑥] +[②⑦] +[②⑧] +[②⑩] +[②B] +[②G] +[②] +[②a] +[②b] +[②c] +[②d] +[②e] +[②f] +[②g] +[②h] +[②i] +[②j] +[③①] +[③⑩] +[③F] +[③] +[③a] +[③b] +[③c] +[③d] +[③e] +[③g] +[③h] +[④] +[④a] +[④b] +[④c] +[④d] +[④e] +[⑤] +[⑤]] +[⑤a] +[⑤b] +[⑤d] +[⑤e] +[⑤f] +[⑥] +[⑦] +[⑧] +[⑨] +[⑩] +[*] +[- +[] +] +]∧′=[ +][ +_ +a] +b] +c] +e] +f] +ng昉 +{ +{- +| +} +}> +~ +~± +~+ +¥ \ No newline at end of file diff --git a/XmindCopilot/topic_cluster/TextCluster/search.py b/XmindCopilot/topic_cluster/TextCluster/search.py new file mode 100644 index 0000000..22d7253 --- /dev/null +++ b/XmindCopilot/topic_cluster/TextCluster/search.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +import os +import argparse +import pickle + +from utils.similar import jaccard +from utils.segmentor import Segmentor +from utils.utils import check_file, get_stop_words, Range + + +def _get_parser(): + parser = argparse.ArgumentParser() + parser.add_argument('--infile', type=str, default='./data/output', help='Directory of input file.') + parser.add_argument('--dict', type=str, default='./data/seg_dict', help='Directory of dict file.') + parser.add_argument('--stop_words', type=str, default='./data/stop_words', help='Directory of stop words.') + parser.add_argument('--top_k', type=int, default=3, help='Return k item.') + parser.add_argument('--sim_th', type=float, default=1.0, choices=[Range(0.5, 1.0)], + help='Threshold for word similarity.') + parser.add_argument('--threshold', type=float, default=0.3, choices=[Range(0.0, 1.0)], + help='Threshold for matching.') + parser.add_argument('--lang', type=str, choices=['cn', 'en'], default='cn', help='Segmentor language setting.') + args = parser.parse_args() + return args + + +class Searcher(object): + def __init__(self, args=_get_parser()): + p_bucket_path = os.path.join(args.infile, 'p_bucket.pickle') + with open(p_bucket_path, 'rb') as infile: + self.p_bucket = pickle.load(infile) + self.seg = Segmentor(args) + self.path = args.infile + self.sim_th = args.sim_th + self.stop_words = get_stop_words(args.stop_words) + self.args = args + + def search(self, sentence): + if not sentence or type(sentence) != str: + return None + res = list() + c_bucket = list() + seg_sen = list(self.seg.cut(sentence)) + seg_sen = list(filter(lambda x: x not in self.stop_words, seg_sen)) + for w in seg_sen: + if w in self.p_bucket: + c_bucket += self.p_bucket[w] + c_bucket = list(set(c_bucket)) + cmp, score = list(), list() + for bucket in c_bucket: + bucket_path = os.path.join(self.path, bucket) + check_file(bucket_path) + infile = open(bucket_path, 'r', encoding="utf-8") + for inline in infile: + inline = inline.rstrip() + line = inline.split(':::')[0] + seg_list = list(self.seg.cut(line)) + seg_list = list(filter(lambda x: x not in self.stop_words, seg_list)) + sc = jaccard(seg_sen, seg_list) + if sc < self.args.threshold: + continue + cmp.append(inline) + score.append(sc) + infile.close() + + zipped = zip(cmp, score) + zipped = sorted(zipped, key=lambda x: x[1], reverse=True) + right = None if self.args.top_k <= 0 else self.args.top_k + for (cp, sc) in zipped[:right]: + res.append(cp) + return res + + +if __name__ == '__main__': + sea = Searcher() + res = sea.search('我是') + print(res) diff --git a/XmindCopilot/topic_cluster/TextCluster/utils/__init__.py b/XmindCopilot/topic_cluster/TextCluster/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/XmindCopilot/topic_cluster/TextCluster/utils/segmentor.py b/XmindCopilot/topic_cluster/TextCluster/utils/segmentor.py new file mode 100644 index 0000000..0f276fc --- /dev/null +++ b/XmindCopilot/topic_cluster/TextCluster/utils/segmentor.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +import os +import argparse + + +def _get_parser(): + parser = argparse.ArgumentParser() + parser.add_argument('--dict', type=str, default='../data/seg_dict', help='Directory of dict file.') + parser.add_argument('--lang', type=str, choices=['cn', 'en'], default='cn', help='Segmentor language setting.') + args = parser.parse_args() + return args + + +class Segmentor(object): + """ + Wrapper for bilingual tokenizer + """ + def __init__(self, args): + if args.lang == 'cn': + import jieba + if args.dict: + if not os.path.exists(args.dict): + print('Segmentor dictionary not found.') + exit(1) + jieba.load_userdict(args.dict) + self.cut = jieba.cut + else: # en + from spacy.tokenizer import Tokenizer + from spacy.lang.en import English + nlp = English() + self.tokenizer = Tokenizer(nlp.vocab) + self.cut = self.cut_en + + def cut_en(self, sentence): + words = self.tokenizer(sentence) + words = [w.text for w in words] + return words + + +if __name__ == '__main__': + args = _get_parser() + args.lang = 'cn' + seg = Segmentor(args) + res = list(seg.cut('你是李小龙吗?')) + print(res) + args.lang = 'en' + seg = Segmentor(args) + res = list(seg.cut('Are you Bruce Lee?')) + print(res) diff --git a/XmindCopilot/topic_cluster/TextCluster/utils/similar.py b/XmindCopilot/topic_cluster/TextCluster/utils/similar.py new file mode 100644 index 0000000..e0e632d --- /dev/null +++ b/XmindCopilot/topic_cluster/TextCluster/utils/similar.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- + + +def jaccard(a, b): + """ + Calculate Jaccard similarity. + :param a: sentence1, list of segmented words + :param b: sentence2 + :return: similar score + """ + a = set(a) + b = set(b) + c = a.intersection(b) + return float(len(c)) / (len(a) + len(b) - len(c)) diff --git a/XmindCopilot/topic_cluster/TextCluster/utils/utils.py b/XmindCopilot/topic_cluster/TextCluster/utils/utils.py new file mode 100644 index 0000000..5ed888e --- /dev/null +++ b/XmindCopilot/topic_cluster/TextCluster/utils/utils.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- +import os +import re +import random + + +class Range(object): + def __init__(self, start, end=None): + self.start = start + self.end = end + + def __eq__(self, other): + if self.end: + return self.start <= other <= self.end + else: + return self.start <= other + + +def check_file(file): + if file is not None and not os.path.exists(file): + print("File {} does not exist. Exit.".format(file)) + exit(1) + + +def ensure_dir(d, verbose=True): + if not os.path.exists(d): + if verbose: + print("Directory {} do not exist; creating...".format(d)) + os.makedirs(d) + + +def clean_dir(d, l=9): + """ + should be identical to file naming pattern + """ + file_list = os.listdir(d) + name_pattern = '[0-9]{' + str(l) + '}$' + file_list = list(filter(lambda x: re.search(name_pattern, x), file_list)) + for fname in file_list: + file_path = os.path.join(d, fname) + try: + if os.path.isfile(file_path): + os.unlink(file_path) + except Exception as e: + print(e) + + +def line_counter(d): + with open(d, 'r', encoding='utf-8') as infile: + line_cnt = sum(1 for _ in infile) + return line_cnt + + +def sample_file(filename, k=5): + """ + Random select k lines from input file. + :param filename: input file directory. + :param k: selected number. + :return: list of lines + """ + selected = list() + line_cnt = line_counter(filename) + with open(filename, 'r', encoding='utf-8') as infile: + if line_cnt <= k: + selected = list(map(lambda x: x.rstrip(), infile.readlines())) + + else: + # generate k random number and sort them + random_index = sorted(random.sample(range(line_cnt), k), reverse=True) + select_index = random_index.pop() + for idx, line in enumerate(infile): + if idx == select_index: + selected.append(line.rstrip()) + if len(random_index) > 0: + select_index = random_index.pop() + else: + break + + return selected + + +def get_stop_words(d): + with open(d, 'r', encoding='utf-8') as infile: + data = infile.readlines() + data = list(map(lambda x: x.rstrip(), data)) + data.append(' ') # manual insert + return data + + +if __name__ == '__main__': + res = sample_file('../data/infile') + print(res) + res = sample_file('../data/seg_dict') + print(res) + diff --git a/XmindCopilot/topic_cluster/__init__.py b/XmindCopilot/topic_cluster/__init__.py new file mode 100644 index 0000000..4963419 --- /dev/null +++ b/XmindCopilot/topic_cluster/__init__.py @@ -0,0 +1,36 @@ +from ..core.topic import TopicElement +from ..core.markerref import MarkerId +from ..search import topic_search +from .TextCluster.cluster import cluster, ClusterArgs + + +def topic_cluster(topic, recursive=False, seg_line_markerId=MarkerId.flagRed, args=ClusterArgs()): + topic.removeSubTopicbyMarkerId(seg_line_markerId) + topics = topic.getSubTopics() + if recursive: + for t in topics: + topic_cluster(t, recursive, seg_line_markerId, args) + if len(topics) > 1: + # TODO: Multi-line title needs to be handled + # (although it might not cause problems temporarily) + namelist = [t.getTitle() for t in topics] + cluster_result = cluster(args, namelist) + for c in cluster_result: + for title in c: + if title: + t = topic_search(topic, title) + if t: + t.moveTopic(-1) + else: + print("failed to search:", title) + tmptopic = TopicElement( + ownerWorkbook=topic.getOwnerWorkbook(), title="———") + tmptopic.addMarker(seg_line_markerId) + topic.addSubTopic(tmptopic) + + +if __name__ == "__main__": + # import numpy + # data = cluster(args, ret_output=True) + # print(data[0]) + pass diff --git a/xmind/utils.py b/XmindCopilot/utils.py similarity index 89% rename from xmind/utils.py rename to XmindCopilot/utils.py index b07fea1..d554ccd 100644 --- a/xmind/utils.py +++ b/XmindCopilot/utils.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- """ - xmind.utils provide a handy way for internal used by xmind, + XmindCopilot.utils provide a handy way for internal used by xmind, and excepted that function defined here will be useful to others. """ @@ -138,3 +138,12 @@ def wrapper(self, *args, **kwargs): return None return wrapper return decorator + +# ***************Other**************** +# 列表去重复 +def remove_duplicates(thy_list, sort=False): + my_set = set(thy_list) # 集合有去重功能,将列表转换成集合 + my_list = list(my_set) # 将集合转换成列表,列表实现去重 + if sort: + my_list.sort() + return my_list \ No newline at end of file diff --git a/apps/github_mgr/.gitignore b/apps/github_mgr/.gitignore new file mode 100644 index 0000000..e91e4e7 --- /dev/null +++ b/apps/github_mgr/.gitignore @@ -0,0 +1 @@ +user_cfg.py \ No newline at end of file diff --git a/apps/github_mgr/github_manager.py b/apps/github_mgr/github_manager.py new file mode 100644 index 0000000..a1ed11b --- /dev/null +++ b/apps/github_mgr/github_manager.py @@ -0,0 +1,194 @@ +''' +Author: Raymon Yip 2205929492@qq.com +Date: 2025-03-09 11:57:27 +Description: file content +FilePath: /XmindCopilot/apps/github_mgr/github_manager.py +LastEditTime: 2025-03-10 11:26:57 +LastEditors: Raymon Yip +''' +import subprocess +import requests +import json +import os +import XmindCopilot +from XmindCopilot.search import topic_search_by_title, topic_search_by_hyperlink +from XmindCopilot.topic_cluster import topic_cluster +from user_cfg import user_name, token + +# Directory Management +try: + # Run in Terminal + ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) +except: + # Run in ipykernel & interactive + ROOT_DIR = os.getcwd() + + +def get_github_stars(username, token=None): + """ + Retrieve a list of starred repositories for a GitHub user + + Parameters: + username (str): GitHub username to query + token (str, optional): GitHub personal access token for authentication + + Returns: + list: Repository full names in format 'owner/repo' + + Example: + >>> get_github_stars('octocat') + ['torvalds/linux', 'github/docs'] + """ + headers = {'Accept': 'application/vnd.github.v3+json'} + if token: + headers['Authorization'] = f'token {token}' + + url = f"https://api.github.com/users/{username}/starred" + + star_list = [] + page_cnt = 0 + while url: + try: + print(f"Requesting page {page_cnt} of starred repositories...") + response = requests.get(url, headers=headers) + response.raise_for_status() + data = response.json() + star_list.extend(data) + page_cnt += 1 + + # Handle pagination + if 'next' in response.links: + url = response.links['next']['url'] + else: + url = None + + except requests.exceptions.RequestException as e: + raise RuntimeError(f"API request failed: {str(e)}") from e + + return star_list + + +# JSON fields +# assignableUsers, codeOfConduct, contactLinks, createdAt, defaultBranchRef, +# deleteBranchOnMerge, description, diskUsage, forkCount, fundingLinks, +# hasDiscussionsEnabled, hasIssuesEnabled, hasProjectsEnabled, hasWikiEnabled, +# homepageUrl, id, isArchived, isBlankIssuesEnabled, isEmpty, isFork, +# isInOrganization, isMirror, isPrivate, isSecurityPolicyEnabled, isTemplate, +# isUserConfigurationRepository, issueTemplates, issues, labels, languages, +# latestRelease, licenseInfo, mentionableUsers, mergeCommitAllowed, milestones, +# mirrorUrl, name, nameWithOwner, openGraphImageUrl, owner, parent, +# primaryLanguage, projects, projectsV2, pullRequestTemplates, pullRequests, +# pushedAt, rebaseMergeAllowed, repositoryTopics, securityPolicyUrl, +# squashMergeAllowed, sshUrl, stargazerCount, templateRepository, updatedAt, url, +# usesCustomOpenGraphImage, viewerCanAdminister, viewerDefaultCommitEmail, +# viewerDefaultMergeMethod, viewerHasStarred, viewerPermission, +# viewerPossibleCommitEmails, viewerSubscription, visibility, watchers + +DEFAULT_FIELDS = [ + 'name', 'owner', 'description', + 'isPrivate', 'isFork', 'isTemplate', 'isArchived', + 'stargazerCount', 'forkCount', 'watchers', + 'url', 'homepageUrl', 'sshUrl', + # 'visibility', 'viewerHasStarred', + # 'createdAt', 'updatedAt', +] + + +def get_repo_str(): + limit = 1000 + cmd = f'gh repo list -L {limit} --json ' + ','.join(DEFAULT_FIELDS) + result = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE) + return result.stdout.decode('utf-8') + + +def get_repo_list(): + repo_list = get_repo_str() + return json.loads(repo_list) + + +class RepoManager(object): + def __init__(self, xmind_dir, user_name, token=None): + self.user_name = user_name + self.token = token + self.load_xmind(xmind_dir) + + def load_xmind(self, xmind_dir): + self.workbook = XmindCopilot.load(xmind_dir) + self.repo_node = topic_search_by_title(self.workbook.getSheets()[2].getRootTopic(), "Repos", 1) + self.star_node = topic_search_by_title(self.workbook.getSheets()[2].getRootTopic(), "Stars", 1) + + def save_repo_list(self, filename='repo_list.json'): + with open(os.path.join(ROOT_DIR, filename), 'w') as f: + f.write(json.dumps(self.repo_list, indent=4)) + + def save_star_list(self, filename='star_list.json'): + with open(os.path.join(ROOT_DIR, filename), 'w') as f: + f.write(json.dumps(self.star_list, indent=4)) + + def update_repo_node(self): + self.repo_list = get_repo_list() + if not topic_search_by_title(self.repo_node, "New", 1): + self.repo_node.addSubTopicbyTitle("New") + new_node = topic_search_by_title(self.repo_node, "New", 1) + for repo in self.repo_list: + # ●: Public Repo, ○: Private Repo, + # ■: Archived Public Repo, □: Archived Private Repo, ▲: Forked Repo + prefix = "" + if not repo['isPrivate'] and not repo['isArchived']: + prefix = "●" + elif repo['isPrivate'] and not repo['isArchived']: + prefix = "○" + elif not repo['isPrivate'] and repo['isArchived']: + prefix = "■" + elif repo['isPrivate'] and repo['isArchived']: + prefix = "□" + if repo['isFork']: + prefix = "▲" + + title = prefix + " " + repo['name'] + "\n" + \ + "★" + str(repo['stargazerCount']) + "|" + \ + "F" + str(repo['forkCount']) + "|" + \ + "W" + str(repo['watchers']['totalCount']) + url = repo['url'] + repo_node = topic_search_by_hyperlink(self.repo_node, url, -1) + if not repo_node: + repo_node = new_node.addSubTopicbyTitle(title) + repo_node.setHyperlink(url) + elif repo_node.getTitle() != title: + repo_node.setTitle(title) + if len(new_node.getSubTopics()) == 0: + new_node.removeTopic() + + def update_star_node(self, arrange_by_owner=True, cluster=True): + self.star_list = get_github_stars(self.user_name, self.token) + if not topic_search_by_title(self.star_node, "New", 1): + self.star_node.addSubTopicbyTitle("New") + new_node = topic_search_by_title(self.star_node, "New", 1) + for star in self.star_list: + title = star['full_name'] + url = star['html_url'] + owner_name = star['owner']['login'] + if not topic_search_by_hyperlink(self.star_node, url, -1): + if arrange_by_owner: + owner_node = topic_search_by_title(new_node, owner_name, 1) + if not owner_node: + owner_node = new_node.addSubTopicbyTitle(owner_name) + owner_node.setFolded() + star_node = owner_node.addSubTopicbyTitle(title) + star_node.setHyperlink(url) + else: + star_node = new_node.addSubTopicbyTitle(title) + star_node.setHyperlink(url) + if cluster: + topic_cluster(new_node, recursive=False) + + def save_xmind(self): + XmindCopilot.save(self.workbook) + +if __name__ == '__main__': + xmind_dir = "apps/temp.xmind8" + repo_manager = RepoManager(xmind_dir, user_name, token) + # repo_manager.update_repo_node() + repo_manager.update_star_node(arrange_by_owner=False) + # repo_manager.save_repo_list() + repo_manager.save_xmind() diff --git a/apps/global_search.py b/apps/global_search.py new file mode 100644 index 0000000..4d90f80 --- /dev/null +++ b/apps/global_search.py @@ -0,0 +1,38 @@ +''' +Author: MasterYip 2205929492@qq.com +Date: 2023-12-27 14:45:21 +LastEditors: MasterYip +LastEditTime: 2024-01-06 10:24:39 +FilePath: /XmindCopilot/apps/global_search.py +Description: file content +''' +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# autopep8: off +import os +import sys +import glob +sys.path.append(os.path.abspath(os.path.dirname(os.path.dirname(__file__)))) +from XmindCopilot.search import BatchSearch +# autopep8: on + + +def getXmindPath(): + path = [] + # path += glob.glob('D:/SFTR/**/*.xmind', recursive=True) + path += glob.glob('D:/SFTR/**/*.xmind8', recursive=True) + path += glob.glob('E:/SFTRDatapool2/ProjectCompleted/**/*.xmind', + recursive=True) + path += glob.glob('E:/SFTRDatapool2/ProjectCompleted/**/*.xmind8', + recursive=True) + return path + + +def GlobalSearchLooper(): + while True: + searchstr = input("Search:") + BatchSearch(searchstr, getXmindPath(), True) + + +if __name__ == "__main__": + GlobalSearchLooper() diff --git a/apps/md2xmind.py b/apps/md2xmind.py new file mode 100644 index 0000000..6a458a7 --- /dev/null +++ b/apps/md2xmind.py @@ -0,0 +1,27 @@ + +# -*- coding: utf-8 -*- + +# autopep8: off +import os +import sys +import glob +sys.path.append(os.path.abspath(os.path.dirname(os.path.dirname(__file__)))) +import XmindCopilot +from XmindCopilot.fmt_cvt.md2xmind import MarkDown2Xmind +from XmindCopilot.search import topic_search +# autopep8: on + +if __name__ == "__main__": + xmind_path = "apps/temp.xmind8" + workbook = XmindCopilot.load(xmind_path) + rootTopic = workbook.getPrimarySheet().getRootTopic() + filetreeTopic = topic_search(rootTopic, "头脑风暴", 2) + + md_file = "apps/temp.md" + with open(md_file, "r", encoding="utf-8") as f: + md_content = f.read() + md2xmind = MarkDown2Xmind(filetreeTopic) + md2xmind.convert2xmind(md_content, cvtEquation=True, cvtWebImage=True, cvtHyperLink=True, cvtTable=True) + # md2xmind.printSubSections(md_content) + XmindCopilot.save(workbook) + pass diff --git a/docs/README_en.md b/docs/README_en.md deleted file mode 100644 index 22a3876..0000000 --- a/docs/README_en.md +++ /dev/null @@ -1,62 +0,0 @@ -# XMind - -**XMind** is a a one-stop solution for creating, parsing, and updating XMind files. -To help Python developers to easily work with XMind files and build XMind extensions. - -## Install - -Clone the repository to a local working directory -``` -git clone git@github.com:zhuifengshen/xmind.git -``` - -Now there will be a directory named `xmind` under the current directory. Change to the directory `xmind-sdk-python` and install **XMind**. -``` -pip3 install xmind -``` - -*It is highly recommended to install __XMind__ under an isolated python environment using [pipenv](https://github.com/pypa/pipenv)* - -## Usage - -Open an existing XMind file or create a new XMind file and place it into a given path -``` -import xmind -workbook = xmind.load(/path/to/file/) # Requires '.xmind' extension -``` - -Save XMind file to a path. -If the path is not given then the API will save to the path set in the workbook. -``` -xmind.save(workbook) -``` -or: -``` -xmind.save(workbook, /save/file/to/path) -``` - - -## LICENSE -``` -The MIT License (MIT) - -Copyright (c) 2019 Devin https://zhangchuzhao.site -Copyright (c) 2013 XMind, Ltd - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -``` \ No newline at end of file diff --git a/docs/comments-demo.xml b/docs/comments-demo.xml deleted file mode 100644 index e9d3607..0000000 --- a/docs/comments-demo.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - I'm a comment! - - - Hello comment! - - \ No newline at end of file diff --git a/docs/content-demo.xml b/docs/content-demo.xml deleted file mode 100644 index 8d0717d..0000000 --- a/docs/content-demo.xml +++ /dev/null @@ -1,100 +0,0 @@ - - - - - root node - - - - first sub topic - - - - I'm a sub topic too - - - - - - - - - - - - - second sub topic - - - third sub topic - - - fourth sub topic - - - - - detached topic - - - - - - first sheet - - - - root node - - - - redirection to the first sheet - - - - sub topic - - - - - - - - - - - - second node - - - - - - topic with notes - - notes for this topic - - - - - - - topic with a file - - - - - - - - second sheet - - - test - - - 联系 - - - - \ No newline at end of file diff --git a/docs/styles-demo.xml b/docs/styles-demo.xml deleted file mode 100644 index bca585f..0000000 --- a/docs/styles-demo.xml +++ /dev/null @@ -1,107 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/docs/xmind_file_structure.xmind b/docs/xmind_file_structure.xmind deleted file mode 100644 index bb71b1e..0000000 Binary files a/docs/xmind_file_structure.xmind and /dev/null differ diff --git a/docs/xmind_native_elements.xmind b/docs/xmind_native_elements.xmind deleted file mode 100644 index 0afae42..0000000 Binary files a/docs/xmind_native_elements.xmind and /dev/null differ diff --git a/example/create_xmind.py b/example/create_xmind.py deleted file mode 100644 index b1dc51f..0000000 --- a/example/create_xmind.py +++ /dev/null @@ -1,113 +0,0 @@ -# -*- coding: utf-8 -*- - -import xmind -from xmind.core.const import TOPIC_DETACHED -from xmind.core.markerref import MarkerId -from xmind.core.topic import TopicElement - - -def gen_my_xmind_file(): - # load an existing file or create a new workbook if nothing is found - workbook = xmind.load("my.xmind") - # get the first sheet(a new workbook has a blank sheet by default) - sheet1 = workbook.getPrimarySheet() - design_sheet1(sheet1) - # create sheet2 - gen_sheet2(workbook, sheet1) - # now we save as test.xmind - xmind.save(workbook, path='test.xmind') - - -def design_sheet1(sheet1): - # ***** first sheet ***** - sheet1.setTitle("first sheet") # set its title - - # get the root topic of this sheet(a sheet has a blank root topic by default) - root_topic1 = sheet1.getRootTopic() - root_topic1.setTitle("root node") # set its title - - # create some sub topic element - sub_topic1 = root_topic1.addSubTopic() - sub_topic1.setTitle("first sub topic") - - sub_topic2 = root_topic1.addSubTopic() - sub_topic2.setTitle("second sub topic") - - sub_topic3 = root_topic1.addSubTopic() - sub_topic3.setTitle("third sub topic") - - sub_topic4 = root_topic1.addSubTopic() - sub_topic4.setTitle("fourth sub topic") - - # create a detached topic(attention: only root topic can add a detached topic) - detached_topic1 = root_topic1.addSubTopic(topics_type=TOPIC_DETACHED) - detached_topic1.setTitle("detached topic") - detached_topic1.setPosition(0, 30) - - sub_topic1_1 = sub_topic1.addSubTopic() - sub_topic1_1.setTitle("I'm a sub topic too") - - -def gen_sheet2(workbook, sheet1): - # ***** second sheet ***** - # create a new sheet and add to the workbook by default - sheet2 = workbook.createSheet() - sheet2.setTitle("second sheet") - - # a sheet has a blank sheet by default - root_topic2 = sheet2.getRootTopic() - root_topic2.setTitle("root node") - - # use other methods to create some sub topic element - topic1 = TopicElement(ownerWorkbook=workbook) - # set a topic hyperlink from this topic to the first sheet given by s1.getID() - topic1.setTopicHyperlink(sheet1.getID()) - topic1.setTitle("redirection to the first sheet") # set its title - - topic2 = TopicElement(ownerWorkbook=workbook) - topic2.setTitle("topic with an url hyperlink") - topic2.setURLHyperlink("https://github.com/zhuifengshen/xmind") # set an url hyperlink - - topic3 = TopicElement(ownerWorkbook=workbook) - topic3.setTitle("third node") - topic3.setPlainNotes("notes for this topic") # set notes (F4 in XMind) - topic3.setTitle("topic with \n notes") - - topic4 = TopicElement(ownerWorkbook=workbook) - topic4.setFileHyperlink("logo.png") # set a file hyperlink - topic4.setTitle("topic with a file") - - topic1_1 = TopicElement(ownerWorkbook=workbook) - topic1_1.setTitle("sub topic") - topic1_1.addLabel("a label") # official XMind only can a one label - - topic1_1_1 = TopicElement(ownerWorkbook=workbook) - topic1_1_1.setTitle("topic can add multiple markers") - topic1_1_1.addMarker(MarkerId.starBlue) - topic1_1_1.addMarker(MarkerId.flagGreen) - - topic2_1 = TopicElement(ownerWorkbook=workbook) - topic2_1.setTitle("topic can add multiple comments") - topic2_1.addComment("I'm a comment!") - topic2_1.addComment(content="Hello comment!", author='devin') - - # then the topics must be added to the root element - root_topic2.addSubTopic(topic1) - root_topic2.addSubTopic(topic2) - root_topic2.addSubTopic(topic3) - root_topic2.addSubTopic(topic4) - topic1.addSubTopic(topic1_1) - topic2.addSubTopic(topic2_1) - topic1_1.addSubTopic(topic1_1_1) - - # to loop on the subTopics - topics = root_topic2.getSubTopics() - for index, topic in enumerate(topics): - topic.addMarker("priority-" + str(index + 1)) - - # create a relationship - sheet2.createRelationship(topic1.getID(), topic2.getID(), "relationship test") - - -if __name__ == '__main__': - gen_my_xmind_file() diff --git a/example/demo.xmind b/example/demo.xmind deleted file mode 100644 index b7385a7..0000000 Binary files a/example/demo.xmind and /dev/null differ diff --git a/example/logo.png b/example/logo.png deleted file mode 100644 index 63eaa85..0000000 Binary files a/example/logo.png and /dev/null differ diff --git a/example/parse_xmind.py b/example/parse_xmind.py deleted file mode 100644 index f108507..0000000 --- a/example/parse_xmind.py +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env python -# _*_ coding:utf-8 _*_ -import json - -import xmind -import pipes - - -def custom_parse_xmind(workbook): - elements = {} - - def _echo(tag, element, indent=0): - title = element.getTitle() - elements[element.getID()] = title - print('\t' * indent, tag, ':', pipes.quote(title)) - - def dump_sheet(sheet): - root_topic = sheet.getRootTopic() - _echo('RootTopic', root_topic, 1) - - for topic in root_topic.getSubTopics() or []: - _echo('AttachedSubTopic', topic, 2) - - for topic in root_topic.getSubTopics(xmind.core.const.TOPIC_DETACHED) or []: - _echo('DetachedSubtopic', topic, 2) - - for rel in sheet.getRelationships(): - id1, id2 = rel.getEnd1ID(), rel.getEnd2ID() - print('Relationship: [%s] --> [%s]' % (elements.get(id1), elements.get(id2))) - - for sheet in workbook.getSheets(): - _echo('Sheet', sheet) - dump_sheet(sheet) - - -def dict_to_prettify_json(data): - print(json.dumps(data, indent=4, separators=(',', ': '))) - - -def main(): - # 1、you can convert the xmind file to dict data or json data - workbook = xmind.load('demo.xmind') - print(workbook.getData()) - print(workbook.to_prettify_json()) - - # 2、you can also convert the sheet to dict data - sheet = workbook.getPrimarySheet() - dict_to_prettify_json(sheet.getData()) - - # 3、as well as topic - root_topic = sheet.getRootTopic() - dict_to_prettify_json(root_topic.getData()) - - # 4、as well as comments - commentsbook = workbook.commentsbook - print(commentsbook.getData()) - - # 5、custom extraction of required data - custom_parse_xmind(workbook) - - -if __name__ == '__main__': - main() diff --git a/example/update_xmind.py b/example/update_xmind.py deleted file mode 100644 index 7cbff2c..0000000 --- a/example/update_xmind.py +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env python -# _*_ coding:utf-8 _*_ -import xmind -from xmind.core.markerref import MarkerId - - -def update_xmind(): - workbook = xmind.load('demo.xmind') - primary_sheet = workbook.getPrimarySheet() - root_topic = primary_sheet.getRootTopic() - root_topic.addMarker(MarkerId.starRed) - - # 1、save all content and save as xmind_update_demo.xmind(recommended) - xmind.save(workbook=workbook, path='xmind_update_demo.xmind') - - # 2、only save the content.xml - xmind.save(workbook=workbook, path='xmind_update_demo1.xmind', only_content=True) - - # 3、only save content.xml、comments.xml、styles.xml - xmind.save(workbook=workbook, path='xmind_update_demo2.xmind', except_attachments=True) - - # 4、save everything except `Revisions` content to save space(also recommended) - xmind.save(workbook=workbook, path='xmind_update_demo3.xmind', except_revisions=True) - - # 5、update and overwrite the original file directly. - xmind.save(workbook) - - -if __name__ == '__main__': - update_xmind() diff --git a/images/Xmind-Sdk.png b/images/Xmind-Sdk.png deleted file mode 100644 index 5b365f4..0000000 Binary files a/images/Xmind-Sdk.png and /dev/null differ diff --git a/images/first_sheet.png b/images/first_sheet.png deleted file mode 100644 index 660d3a7..0000000 Binary files a/images/first_sheet.png and /dev/null differ diff --git a/images/pypi_upload.png b/images/pypi_upload.png deleted file mode 100644 index 8e03c80..0000000 Binary files a/images/pypi_upload.png and /dev/null differ diff --git a/images/second_sheet.png b/images/second_sheet.png deleted file mode 100644 index a2ea13f..0000000 Binary files a/images/second_sheet.png and /dev/null differ diff --git a/images/testcase_preview.png b/images/testcase_preview.png deleted file mode 100644 index ddbf976..0000000 Binary files a/images/testcase_preview.png and /dev/null differ diff --git a/images/testlink.png b/images/testlink.png deleted file mode 100644 index 0e5f3c3..0000000 Binary files a/images/testlink.png and /dev/null differ diff --git a/images/webtool.png b/images/webtool.png deleted file mode 100644 index 5fe9c35..0000000 Binary files a/images/webtool.png and /dev/null differ diff --git a/images/xmind.png b/images/xmind.png deleted file mode 100644 index 6f7a82c..0000000 Binary files a/images/xmind.png and /dev/null differ diff --git a/images/xmind_file_structure.png b/images/xmind_file_structure.png deleted file mode 100644 index fb62735..0000000 Binary files a/images/xmind_file_structure.png and /dev/null differ diff --git a/images/xmind_native_elements.png b/images/xmind_native_elements.png deleted file mode 100644 index f17f4e5..0000000 Binary files a/images/xmind_native_elements.png and /dev/null differ diff --git a/images/zentao.png b/images/zentao.png deleted file mode 100644 index 9ed31a1..0000000 Binary files a/images/zentao.png and /dev/null differ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f4fa61b Binary files /dev/null and b/requirements.txt differ diff --git a/setup.py b/setup.py index 083a519..b35c635 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ about = {} here = os.path.abspath(os.path.dirname(__file__)) -with io.open(os.path.join(here, 'xmind', '__about__.py'), encoding='utf-8') as f: +with io.open(os.path.join(here, 'XmindCopilot', '__about__.py'), encoding='utf-8') as f: exec(f.read(), about) with io.open('README.md', encoding='utf-8') as f: @@ -73,7 +73,7 @@ def run(self): author_email=about['__author_email__'], url=about['__url__'], license=about['__license__'], - packages=find_packages(exclude=['example', 'tests', 'test.*', 'docs']), + packages=find_packages(exclude=['example', 'test', 'test.*', 'docs']), package_data={'': ['README.md']}, install_requires=install_requires, extras_require={}, diff --git a/test/.gitignore b/test/.gitignore new file mode 100644 index 0000000..1c2f433 --- /dev/null +++ b/test/.gitignore @@ -0,0 +1 @@ +tmp \ No newline at end of file diff --git a/test/Test.html b/test/Test.html new file mode 100644 index 0000000..9bc6bd2 --- /dev/null +++ b/test/Test.html @@ -0,0 +1,24 @@ + + + + + KaTeX Example + + + + +

KaTeX Example

+ +

Rendered equation:

+
+ + + + diff --git a/test/TestIndentList.md b/test/TestIndentList.md new file mode 100644 index 0000000..c24fa5c --- /dev/null +++ b/test/TestIndentList.md @@ -0,0 +1,28 @@ +- asdgfg +- 无序列表1 + - adfg + - 无序列表2 + - 无序j列表2 + - 无序j列表2 + - 无序j列表2 + - 无序j列表2 + - 无序列表3 + - sadgfd + - 无序d列表2 + - 无序d列表2 + - 无序d列表2 + - 无序d列表2 + - 无序d列表2 + - 无序d列表2 + - 无序d列表2 + - 无序d列表2 +- 无序列表1 + - asgfhfg + - 无序列表2 + +1. 有序列表1 +2. 有序列表1 + 1. 有序列表2 + 2. 有序列表2 + asldkjflkjg + asdfdf \ No newline at end of file diff --git a/test/TestTemplate.md b/test/TestTemplate.md new file mode 100644 index 0000000..a16ad26 --- /dev/null +++ b/test/TestTemplate.md @@ -0,0 +1,292 @@ +# SLIP模型 + +## 机器人动力学建模 + +### 机器人物理模型参数整定 + +![img](https://picx.zhimg.com/80/v2-59cb6e0776621d19353d5e5c0744751f_720w.webp?source=1940ef5c) + +通过SolidWorks进行机器人的建模,然后可以通过solidworks导出机器人的urdf和相应的质量惯量参数,但是可能不太准,需要通过称重的方式进行校准。可以通过进行重力补偿控制的方式进行机器人物理模型参数的验证。但是这中间会涉及到摩擦力的处理,现在还没有对机器人关节处的摩擦力进行建模 + +### 机器人运动中的 Equation of Motion + +建立系统的动力学方程有两种方法:牛顿-欧拉方法(通过运动学方程推导)和拉格朗日方法(通过系统能量推导) + +#### 牛顿欧拉方法 + +1. 定义参考坐标系 +2. 画出结构关系图 +3. 对系统受力进行分析得到EOM(分别对平动和旋转进行分析) + +$$ +\sum F = \frac{d(mv)}{dt} = m \ddot{x} \\ +\sum T = \frac{d(I \omega)}{dt} = I \ddot{\theta} +$$ + +#### 拉格朗日方程方法 + +1. 定义广义坐标系$q$ +2. 建立系统动能$T$、系统势能$U$、广义力$Q$ +3. 建立拉格朗日项$L = T - U$ +4. 形成系统动力学方程(其中q是描述系统的多个变量,有多少个变量就能形成多少个动力学方程),利用欧拉-拉格朗日方程 + +$$ +Q = \frac{\partial}{\partial t}(\frac{\partial L}{\partial \dot{q}}) - \frac{\partial L}{\partial q} = \frac{\partial}{\partial t}(\frac{\partial L}{\partial \dot{q}}) - \frac{\partial T}{\partial q} + \frac{\partial U}{\partial q} +$$ + +一个利用拉格朗日方法求EoM的例子 simple pendulum + +![image-20230710143929838](https://ultramarine-image.oss-cn-beijing.aliyuncs.com/img/image-20230710143929838.png) + +### 轨迹优化 + +机器人的轨迹优化的目的是对未来期望状态的规划控制,产生所需的前馈信号让系统完成复杂的轨迹运动,反馈控制是用来修正轨迹过程中的误差。轨迹不完全是空间中的一个三维轨迹,也可以是系统状态,机器人的每个关节如何运动 + +轨迹优化分为离线与在线,因为像MPC一样考虑未来很长时间运动变化的对计算的要求很高,没法做到实时规划,所以离线规划,再通过前馈+反馈进行控制。而在实时规划中,要减小计算量就要采用简化模型,比如跳跃采用的SLIP模型产生轨迹,利用前馈+反馈进行跟踪 + +### SLIP模型 + +SLIP模型经典的就是分为两个状态,支撑相和飞行相。对于飞行相主要是受到重力影响 + +人型机器人控制中walking利用ballistic walking inverted pendulum模型,质心在行走过程的中点达到最大高度,runnng采用spring-mass model,质点在跑步过程的中点达到最低高度 + +image-20230706111857952 + +#### EoM of SLIP Model + +$$ +\left\{ +\begin{array}{} +\ddot{\theta} - gl^{-1}\sin(\theta - \gamma) = 0\\ +\ddot{\phi} + gl^{-1} \sin\phi\cos(\theta - \gamma) = \ddot{\theta} +\end{array} +\right. +$$ + +image-20230706120022138 + +forward kinematics + +![image-20230707173151398](https://ultramarine-image.oss-cn-beijing.aliyuncs.com/img/image-20230707173151398.png) + +![image-20230707173823580](https://ultramarine-image.oss-cn-beijing.aliyuncs.com/img/image-20230707173823580.png) + +- asdgfg +- 无序列表1 + - adfg + - 无序列表2 + - 无序j列表2 + - 无序列表3 + - sadgfd + - 无序d列表2 +- 无序列表1 + - asgfhfg + - 无序列表2 + +1. 有序列表1 +2. 有序列表1 + 1. 有序列表2 + 2. 有序列表2 + asldkjflkjg + asdfdf + +#### 1D-SLIP Equation of Motion + +$$ +X = +\begin{bmatrix} +x \\ +\dot{x} +\end{bmatrix} +$$ + + + +- 飞行相 + +$$ +\dot{X} = +\begin{bmatrix} +0 & 1\\ +0 & 0 \\ +\end{bmatrix} + X + + \begin{bmatrix} + 0 \\ + -g \\ + \end{bmatrix} +$$ + +- 支撑相 + +$$ +\dot{X} = +\begin{bmatrix} +0 & 1\\ +-\frac{K}{m} & -D\\ +\end{bmatrix} +X + +\begin{bmatrix} +0 \\ +-g +\end{bmatrix} +$$ + +其中K是弹簧刚度 D是弹簧阻尼系数 + +#### 2D-SLIP Equation of Motion + +![image-20230710151755125](https://ultramarine-image.oss-cn-beijing.aliyuncs.com/img/image-20230710151755125.png) + +- 飞行相 + +定义状态 +$$ +X = +\begin{bmatrix} +x \\ +z \\ +\dot{x} \\ +\dot{z} \\ +\end{bmatrix}, +\qquad +\dot{X} = +\begin{bmatrix} +\dot{x} \\ +\dot{z} \\ +0 \\ +-g\\ +\end{bmatrix} +$$ + +- 支撑相 + +$$ +X = +\begin{bmatrix} +r \\ +\theta \\ +\dot{r} \\ +\dot{\theta} \\ +\end{bmatrix} +$$ + + + +通过拉格朗日方程建立系统运动方程 +$$ +\begin{aligned} +T = \frac{1}{2}mr^{2}\dot{\theta}^{2} + \frac{1}{2}m \dot{r}^{2}\\ +U = \frac{1}{2}k(l_{0} - r)^{2} + mgr\cos{\theta} \\ +L = T - U +\end{aligned} +$$ +通过欧拉-拉格朗日公式得出系统的运动学方程为 +$$ +\begin{aligned} +m \ddot{r} - mr\dot{\theta}^{2} - k(l_{0} - r) + mg \cos{\theta} = 0\\ +2 m \dot{\theta} r \dot{r} + mr^{2}\ddot{\theta} - mgr\sin{\theta} = 0 +\end{aligned} +$$ + +得出的系统更新方程为 +$$ +\begin{aligned} +\ddot{r} = r \dot{\theta}^{2} + \frac{k}{m}(l_{0} - r) - g \cos \theta \\ +\ddot{\theta} = \frac{g}{r}\sin \theta - \frac{2\dot{\theta}}{r}\dot{r} +\end{aligned} +$$ + + +- 从飞行相到支撑相的转换条件 + +$$ +z - l_{0}\cos \theta \leq 0 +$$ + +​ 因为使用了不同的状态变量,因此在切换状态的时候还要进行状态的更新 + +​ 这个状态切换的部分可以用drake中的`MakeWitnessFunction`来实现,very elegant + +​ 使用的公式为: +$$ +\begin{aligned} +&x = -r\sin \theta, \quad z = r\cos \theta \\ +\Rightarrow & \dot{x} = -\dot{r}\sin \theta - r \dot{\theta} \cos \theta \\ +\Rightarrow & \dot{z} = \dot{r}\cos \theta - r \dot{\theta}\sin \theta \\ +\end{aligned} +$$ + +$$ +\begin{aligned} +r^{2} = x^{2} + z^{2} \\ +\Rightarrow \dot{r} = \sin \theta \cdot \dot{x} + \cos\theta \cdot \dot{z} +\end{aligned} +$$ + +最后 +$$ +\begin{aligned} +\dot{r} = \sin \theta \cdot \dot{x} + \cos \theta \cdot \dot{z} \\ +\dot{\theta} = - \frac{1}{r \cos \theta}(\dot{r} \sin\theta + \dot{x}) +\end{aligned} +$$ + +- 从支撑相到飞行相的转化条件 + +$$ +r \geq l_{0} +$$ + +​ 更新的时候使用的方程为 +$$ +\begin{aligned} +&x = -r\sin \theta, \quad z = r\cos \theta \\ +\Rightarrow & \dot{x} = -\dot{r}\sin \theta - r \dot{\theta} \cos \theta \\ +\Rightarrow & \dot{z} = \dot{r}\cos \theta - r \dot{\theta}\sin \theta \\ +\end{aligned} +$$ + +​ 通过再对上面的方程进行求导可以得到质心的更新方程 +$$ +\begin{aligned} +\ddot{x} = -\ddot{l}\sin\theta - 2 \dot{\theta}\dot{l}\cos\theta + l\ddot{\theta}\cos\theta + l \dot{\theta}^{2}\sin\theta \\ +\ddot{z} = \ddot{l}\cos\theta - 2 \dot{l}\dot{\theta}\sin\theta - \ddot{\theta}l\sin\theta - l\dot{\theta}\cos\theta +\end{aligned} +$$ +#### 单足SLIP规划 + +这部分为`slip_jump.cc`中`Trajectory`类`Update`函数行为的分析,这部分规划的是单足机器人利用SLIP模型进行跳跃。Trajectory类的输入为机器人现在的状态`state_est`,进行实时规划出机器人下一步的轨迹,并输出机器人在下一次更新前的预期状态`state_des`。 + +> 程序中`getTheta`函数的运行逻辑 为什么会用到该函数 +> +> getTheta的作用是从空中飞行的速度映射到落地的角度,在空中飞行的过程中调整落地脚的位置和速度 +> +> 后面用一个近似直线函数来拟合,在一定范围内可以看成是近似直线 + +在轨迹规划的过程中,用上一状态的pre_state_des来更新下一状态的信息,在这个过程中前馈不利用测量估计的状态信息,然后在flight的过程中因为要对控制的腿进行姿态的调整,对长度和角度都要添加反馈器,利用两个反馈器对前馈规划进行反馈。 + +打算在进行状态转移的时候 从支撑相到飞行相 飞行相到支撑相的时候将机器人的实际状态赋值给期望状态,进行下一阶段的规划 + +#### 动平衡 + +initial thought: 如果只是小幅度的扰动就都在 controller 的鲁棒性范围内,但是如果涉及大幅度的扰动就要通过轨迹规划来实现机器人的迈步的减少动量的范畴,通过 MPC 实现 + +# 二、函数 + +## 1、get获取原始指针 + +```cpp +std::unique_ptr a = std::make_unique(666); int* b = a.get();std::cout << b << std::endl; +``` + +## 2、reset释放智能指针 + +```cpp +std::unique_ptr a = std::make_unique(666); //释放内存,同时将a置0,所以不会出现悬挂指针的问题a.reset();std::cout << a << std::endl; +``` + +## 3、release将指针置0 +``` +std::unique_ptr a = std::make_unique(666); // 虽然这个函数名叫release,但是并不会真的释放内存,只是把指针置0// 而原来的那片装着666的内存依然存在,但是该函数会返回装着666的内存地址// 综上:相当于先get,然后再resetint* b = a.release();std::cout << a << std::endl;std::cout << b << std::endl; +``` \ No newline at end of file diff --git a/test/TestTemplate.xmind b/test/TestTemplate.xmind new file mode 100644 index 0000000..3fe3f46 Binary files /dev/null and b/test/TestTemplate.xmind differ diff --git a/test/XmindCopilot_test.py b/test/XmindCopilot_test.py new file mode 100644 index 0000000..ab3a122 --- /dev/null +++ b/test/XmindCopilot_test.py @@ -0,0 +1,208 @@ + +import os +import sys +import unittest + +sys.path.append(os.path.abspath(os.path.dirname(os.path.dirname(__file__)))) +# Support CLI pytest (Import error) +import XmindCopilot +from XmindCopilot.search import topic_search +from XmindCopilot.file_shrink import xmind_shrink +from XmindCopilot.fmt_cvt.md2xmind import MarkDown2Xmind +from XmindCopilot.fmt_cvt.latex_render import latex2img +from XmindCopilot.fmt_cvt.latex_render import latex2img_web +from XmindCopilot.topic_cluster import topic_cluster + +TMP_DIR = os.path.join(os.path.dirname(__file__), "tmp") +TEST_TEMPLATE_XMIND = os.path.join( + os.path.dirname(__file__), "TestTemplate.xmind") +TEST_TEMPLATE_MD = os.path.join(os.path.dirname(__file__), "TestTemplate.md") +TEST_TEMPLATE_MDList = os.path.join( + os.path.dirname(__file__), "TestIndentList.md") + +if not os.path.isdir(TMP_DIR): + os.mkdir(TMP_DIR) + + +class TestXmindCopilot(unittest.TestCase): + def testXmindLoad(self): + xmind_path = TEST_TEMPLATE_XMIND + workbook = XmindCopilot.load(xmind_path) + sheets = workbook.getSheets() + first_sheet = sheets[0] + root_topic = first_sheet.getRootTopic() + print(root_topic.getTitle()) + subtopics = root_topic.getSubTopics() + for topic in subtopics: + print(' ', topic.getTitle()) + self.assertTrue(True) + + +class TestTopicCluster(unittest.TestCase): + def testTopicCluster(self): + xmind_path = TEST_TEMPLATE_XMIND + workbook = XmindCopilot.load(xmind_path) + rootTopic = workbook.getPrimarySheet().getRootTopic() + topic_cluster(rootTopic) + XmindCopilot.save(workbook, os.path.join( + TMP_DIR, "TestTopicCluster.xmind")) + self.assertTrue(True) + + +class TestSearch(unittest.TestCase): + def testSearch(self): + xmind_path = TEST_TEMPLATE_XMIND + workbook = XmindCopilot.load(xmind_path) + sheets = workbook.getSheets() + first_sheet = sheets[0] + root_topic = first_sheet.getRootTopic() + search_topic = topic_search(root_topic, '常用标记') + print("\n") + print(search_topic.getTitle()) + for subtopic in search_topic.getSubTopics(): + print(' ', subtopic.getTitle()) + self.assertTrue(True) + + +class TestXmindShrink(unittest.TestCase): + def testXmindShrink(self): + xmind_path = TEST_TEMPLATE_XMIND + xmind_shrink(xmind_path, + PNG_Quality=10, JPEG_Quality=20, use_pngquant=True, + replace=False, + output_path=os.path.join(TMP_DIR, "TestShrink.xmind")) + self.assertTrue(True) + + +class TestXmindFmtConvert(unittest.TestCase): + def testMarkdown2Xmind(self): + file_path = TEST_TEMPLATE_MD + xmind_path = os.path.join(TMP_DIR, "TestMd2Xmind.xmind") + if os.path.isfile(xmind_path): + os.remove(xmind_path) + workbook = XmindCopilot.load(xmind_path) + rootTopic = workbook.getPrimarySheet().getRootTopic() + markdowntext = open(file_path, 'r', encoding='utf-8').read() + # rootTopic.addSubTopicbyMarkDown(markdowntext) + # rootTopic.convertTitle2WebImage(recursive=True) + MarkDown2Xmind(rootTopic).convert2xmind( + markdowntext, cvtWebImage=True, cvtHyperLink=True) + MarkDown2Xmind(rootTopic).printSubSections(markdowntext) + XmindCopilot.save(workbook) + self.assertTrue(True) + + def testMarkdownList2Xmind(self): + file_path = TEST_TEMPLATE_MDList + xmind_path = os.path.join(TMP_DIR, "TestMdList2Xmind.xmind") + if os.path.isfile(xmind_path): + os.remove(xmind_path) + workbook = XmindCopilot.load(xmind_path) + rootTopic = workbook.getPrimarySheet().getRootTopic() + markdowntext = open(file_path, 'r', encoding='utf-8').read() + rootTopic.addSubTopicbyMarkDown(markdowntext) + # MarkDown2Xmind(rootTopic).convert2xmind(markdowntext) + XmindCopilot.save(workbook) + self.assertTrue(True) + + def testLatexRenderer(self): + text = r'$\sum_{i=0}^\infty x_i$' + latex2img(text, size=48, color=(0.1, 0.8, 0.8), + out=os.path.join(TMP_DIR, "TestLatex.png")) + + text = r'$\sum_{n=1}^\infty\frac{-e^{i\pi}}{2^n}$' + im = latex2img(text, size=48, color=(0.9, 0.1, 0.1)) + # im.show() + + def testLatexRendererWeb(self): + # Example usage + # latex_expression = r"a^2+b^2=c^2" + latex_expression = r""" + $$ + \dot{X} = + \begin{bmatrix} + 0 & 1\\ + -\frac{K}{m} & -D\\ + \end{bmatrix} + X + + \begin{bmatrix} + 0 \\ + -g + \end{bmatrix} + $$ + """ + padding = 50 + image_format = 'png' + try: + path = latex2img_web(latex_expression, output_file=None, + padding=padding, image_format=image_format) + os.system("start %s" % path) + except: + print("Failed to render latex expression. please check network connection.") + + +## Legacy +# from XmindCopilot import xmind +# from XmindCopilot.search import topic_search +# from XmindCopilot.topic_cluster import topic_cluster, ClusterArgs, MarkerId +# from XmindCopilot.fileshrink import xmind_shrink +# import re +# from XmindCopilot.playerone_mgr import topic_info_transfer +# import os + +def resource_cluster(): + args = ClusterArgs() + args.sample_number = 5 + args.threshold = 0.0 + args.name_len = 4 + args.name_len_update = False + + workbook = xmind.load("E:/CodeTestFile/comprehensive-coding/XmindCopilot/test/XmlTest.xmind") + sheets = workbook.getSheets() + if not sheets[0].getTitle(): + print("Failed to open:"+workbook.get_path()) + + root_topic = sheets[2].getRootTopic() + topic = topic_search(root_topic, "Draft") + topic.removeSubTopicbyMarkerId(MarkerId.flagRed, recursive=True) + topic_cluster(topic, recursive=False, args=args) + + xmind.save(workbook) + + +def cluster_distribute(): + # TODO Cluster Distrubute + pass + + +def player_info_transfer(): + workbook = xmind.load('D:/SFTR/PlayerOS/Player One.xmind') + sheets = workbook.getSheets() + if not sheets[0].getTitle(): + print("Failed to open:"+workbook.get_path()) + + root_topic = sheets[0].getRootTopic() + topic = topic_search(root_topic, "文件索引") + topic_info_transfer(topic) + xmind.save(workbook) + + +def batch_shrink(): + # Specify the OR + # folder_path = "D:\\CodeTestFiles\\HITSA-Courses-Xmind-Note" + folder_path = "D:\\SFTR\\1 Course\\MITBlended_AI" + + # Specify the compression level + use_pngquant = True + # CV: 0-9(high-low) | pngquant: 1-100(low-high) + PNG_Quality = 10 + # CV: 0-100(low-high) + JPEG_Quality = 20 + + ''' + ideal for xmind files: PNG_Quality=10, JPEG_Quality=20 + extreme compression: PNG_Quality=1, JPEG_Quality=0 (PNG will lose color(almost B&W?), JPEG will lose color details) + ''' + xmind_shrink(folder_path, PNG_Quality, JPEG_Quality, replace=True, use_pngquant=use_pngquant) + +if __name__ == '__main__': + unittest.main() diff --git a/xmind/core/loader.py b/xmind/core/loader.py deleted file mode 100644 index 214f4b9..0000000 --- a/xmind/core/loader.py +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" -xmind.core.loader -""" -from xmind.core.comments import CommentsBookDocument -from xmind.core.styles import StylesBookDocument -from . import const -from .workbook import WorkbookDocument -from .. import utils - - -class WorkbookLoader(object): - def __init__(self, path): - """ Load XMind workbook from given path - - :param path: path to XMind file. If not an existing file, will not raise an exception. - - """ - super(WorkbookLoader, self).__init__() - self._input_source = utils.get_abs_path(path) - - file_name, ext = utils.split_ext(self._input_source) - - if ext != const.XMIND_EXT: - raise Exception("The XMind filename is missing the '%s' extension!" % const.XMIND_EXT) - - # Input Stream - self._content_stream = None - self._styles_stream = None - self._comments_steam = None - - try: - with utils.extract(self._input_source) as input_stream: - for stream in input_stream.namelist(): - if stream == const.CONTENT_XML: - self._content_stream = utils.parse_dom_string(input_stream.read(stream)) - elif stream == const.STYLES_XML: - self._styles_stream = utils.parse_dom_string(input_stream.read(stream)) - elif stream == const.COMMENTS_XML: - self._comments_steam = utils.parse_dom_string(input_stream.read(stream)) - - except BaseException: - pass - - def get_workbook(self): - """ Parse XMind file to `WorkbookDocument` object and return - """ - path = self._input_source - content = self._content_stream - styles = self._styles_stream - comments = self._comments_steam - stylesbook = StylesBookDocument(node=styles, path=path) - commentsbook = CommentsBookDocument(node=comments, path=path) - workbook = WorkbookDocument(node=content, path=path, stylesbook=stylesbook, commentsbook=commentsbook) - - return workbook - - def get_stylesbook(self): - """ Parse Xmind styles.xml to `StylesBookDocument` object and return - """ - content = self._styles_stream - path = self._input_source - - stylesbook = StylesBookDocument(node=content, path=path) - return stylesbook - - def get_commentsbook(self): - content = self._comments_steam - path = self._input_source - - commentsbook = CommentsBookDocument(node=content, path=path) - return commentsbook -