Написав Repost для DreamWidth
Четвер, 29 Грудень 2016 22:55![[personal profile]](https://www.dreamwidth.org/img/silk/identity/user.png)
Ура, %SUBJ%, котани! Налітай!
Скрипт працює так:
- Додає от такий лінк на сторінку допису:
- По кліку на посилання відкриває
http://www.dreamwidth.org/entry/new
у новому вікні (target="_blank"
) та заповнює два поля: Subject та Body.
Оновлено: з версії 0.8 вміє заповнювати також To-Journal, Icon, та Tags.
Причому можна налаштовувати «шаблони» для перепосту: наприклад, «перепост до спільноти такої-то з юзерпикою такою-то і наперед прописаними тегами».
Known Bugs:
Не вставляється YouTube відео. Спробую побороти, але там багато возні.— Починив у v.0.3- Не розгортаються Cut Tags. Перед перепостом треба все руками розгортати
- З тієї ж причини, Cut Tags у перепощеному дописі не завжди вставляються. Їх треба прописувати руками, якщо перепост робився зі сторінки конкретного допису. А якщо перепост зі сторінки журналу, то всі cut вставляються правильно.
Глючить, якщо пост містить <lj user="..."> — ім'я цього юзера помилково вставляється у шапку перепосту (замість імені того юзера, чий пост ви перепощуєте).— Починив у v.0.2
Зауваження:
- Перше і найголовніше: Ніколи не встановлюйте скрипти від невідомих джерел!
Особливо якщо ви не розумієте код. Tags та To [Journal/Community] не прописує.Прописує з версії 0.8.Можливо, потім придумаю шаблони для перепостів, але поки що цього нема.Зроблено у версії 0.8.- Перевіряв на сторінках індивідуальних журналів (
http://bytebuster.dreamwidth.org/
), комун (http://bitter-onion.dreamwidth.org/
) та індивідуальних дописів (http://bitter-onion.dreamwidth.org/1139072.html
). - Перевіряв лише на Firefox. У кого Chrome, перевірте хто-небудь на Tampermonkey, га? Я не упевнений, що там працюють функції, специфічні для GM.
- Багрепорти приймаються, але я не глибоко дружу з JavaScript, тому не гарантія, що я зможу вилікувати усі можливі проблеми.
Встановлення
- Ставимо собі Greasemonkey;
- Перезапускаємо Firefox;
- Повертаємося до цього допису, виділяємо все тіло скрипта і копіюємо до Clipboard;
- Тиць в кнопку Greasemonkey на Toolbar, тиць New User Script…;
- У новому віконці внизу кнопка Use Script from Clipboard;
- Налаштувати свої шаблони у секції:
// **** SETTINGS ****************************************************
- Зберегти;
- Відкрити будь-яку сторінку на DreamWidth.
// ==UserScript== // @name DreamWidth Reposter // @namespace bytebuster.dreamwidth.org // @description Adds LJ-like Repost functionality for DreamWidth // @include http://*.dreamwidth.org/* // @exclude http://*.dreamwidth.org/profile* // @exclude http://*.dreamwidth.org/calendar* // @exclude http://*.dreamwidth.org/tag/ // @version 0.3 // @grant GM_log // @grant GM_getValue // @grant GM_setValue // @grant GM_deleteValue // ==/UserScript== /* ********************************************************************************** This script has two main functions: * addRepostLinks - triggers on all DreamWidth pages; adds "Repost" links to entries; * populatePostForm - triggers on /entry/new page; fills up the "Post Entry" fields Цей скрипт містить дві основні функції: * addRepostLinks - запускається на усіх сторінках DreamWidth pages; додає кнопку «Перепостити»; * populatePostForm - запускається на сторінці /entry/new page; заповнює поля вводу ********************************************************************************** */ /* * 0.3 fixed applied domain and URL; fixed Youtube video embed minor fix if mistakenly decided that post in community, while it actually is in user's own journal * 0.2 added group/blog references fix bug on other users references * 0.1 Initial reease */ (function(){ // **** SETTINGS **************************************************** // Main settings // Головні налаштування const REPOST_TEXT = "Перепостити"; // Так виглядатиме текст кнопки «Репост» const REPOST_TEMPLATE = 'Перепост допису <lj user="{poster}">: <a href="{link}">{subj}</a>\n{body}'; const REPOST_TEMPLATE_COMMUNITY = 'Перепост допису <lj user="{poster}"> @ <lj user="{community}">: <a href="{link}">{subj}</a>\n{body}'; const DEBUG = false; // **** CONSTANTS *************************************************** // You don't need it unless DreamWidth changes its classes (impossible, yeah) // Вам тут не треба нічого міняти, допоки Дрім не поміняє імена класів (майже нереально) const CLS_ENTRY_TITLE = "entry-title"; const CLS_ENTRY_WRAPPER = "entry-wrapper"; const CLS_ENTRY_POSTER = "entry-poster"; const CLS_LJUSER = "ljuser"; const ATTR_LJUSER = "lj:user"; const CLS_ENTRY_CONTENT = "entry-content"; const CLS_ENTRY_LINKS = "entry-interaction-links"; const ID_INPUT_SUBJECT = "id-subject-0"; const ID_INPUT_BODY = "id-event-1"; const CLS_EMBED = "lj_embedcontent-wrapper"; const POST_URL = "http://www.dreamwidth.org/update"; const POST_URL2 = "/entry/new"; const REPOST_PARAM="repost"; const CONTENT_DEFAULT = ""; const CONTENT_TAG = "DW_REPOST_CONTENT"; // how we save the reposted entry in GM_setValue const REGEX_YOUTUBE_LINK = /^.*(?:youtu\.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/gmi; // /www.youtube.com\/watch?v=(.+?)/gmi; const EMBED_YOUTUBE_TEMPLATE = '<iframe width="640" height="360" src="https://www.youtube.com/embed/{videoID}" frameborder="0" allowfullscreen></iframe>'; // applies a parser on strings and log error if failed function parse(parser, where, description) { var ret; match = parser.exec(where); if( match === null ) { GM_log("Failed to parse " + description + " from text: " + where); ret = "(null)"; } else { ret = match[1]; } return ret; } function DebugMessage( what ) { if(DEBUG) { GM_log(what); } } // checks for URL parameter function getQueryVariable(variable) { var query = window.location.search.substring(1); var vars = query.split("&"); for (var i=0;i<vars.length;i++) { var pair = vars[i].split("="); if (pair[0] == variable) { return pair[1]; } } DebugMessage('Query Variable ' + variable + ' not found'); } // Adds Repost links to all entries on the page // Додає посилання «Перепост» внизу усіх дописів на сторінці function addRepostLinks() { DebugMessage("Adding Repost links begin..."); var entries = document.getElementsByClassName(CLS_ENTRY_WRAPPER); for (var entry_index=0; entry_index<entries.length; entry_index++) { addRepostLink(entries[entry_index]); } DebugMessage("Adding Repost links end"); } // Gets the saved content for repost function getRepostData() { DebugMessage("getRepostData begin"); return GM_getValue(CONTENT_TAG, CONTENT_DEFAULT); } // Sets the content for repost function setRepostData(repostData) { DebugMessage("setRepostData begin " + repostData); // delete the state if it is the default if( repostData == CONTENT_DEFAULT ) { GM_deleteValue(CONTENT_TAG); } else { // set the state otherwise GM_setValue(CONTENT_TAG, repostData); } DebugMessage("setRepostData end"); } // Saves JSON data from the reposted entry // Зберігає допис і його шапку function rememberPost(elEntry) { var elPoster, elLjuser, elTitle, elContent; // elements var entryPoster, entryCommunity, entryLink, entrySubject, entryBody, subdomain; // strings var repostData; // JSON object DebugMessage("rememberPost begin"); subdomain = window.location.host.split(".")[0]; // get Poster userId elPoster = elEntry.getElementsByClassName(CLS_ENTRY_POSTER)[0]; if(elPoster && elPoster.hasChildNodes()) { // we seem to be in a community, parse out poster name from attribute elLjuser = elPoster.getElementsByClassName(CLS_LJUSER)[0]; entryPoster = elLjuser.getAttribute(ATTR_LJUSER); entryCommunity = subdomain; } else { // we seem to be in a journal, parse out poster name from URL (FIXME) entryPoster = subdomain; entryCommunity = ""; } // Quick fix: fix if we mistakenly set community if(entryPoster == entryCommunity) { entryCommunity = ""; } // get Subj and Link elTitle = elEntry.getElementsByClassName(CLS_ENTRY_TITLE)[0]; entryLink = elTitle.getElementsByTagName("a")[0].href; entrySubject = elTitle.textContent; // get Body elContent = elEntry.getElementsByClassName(CLS_ENTRY_CONTENT)[0] .cloneNode(true); // Apply fixes elContent = FixYoutube(elContent); elContent = FixUsers(elContent); entryBody = elContent.innerHTML; repostData = JSON.stringify( { "link" : entryLink, "poster" : entryPoster, "community" : entryCommunity, "subj" : entrySubject, "body" : entryBody } ); DebugMessage("Saving JSON: " + repostData); setRepostData(repostData); DebugMessage("rememberPost end"); } // Fixes User references // Takes a cloned node to fix // Returns it as well function FixUsers(element) { var embeds; // list of embed objects DebugMessage("FixUsers begin"); embeds = element.getElementsByClassName(CLS_LJUSER); for (var embed_index=0; embed_index<embeds.length; embed_index++) { var elLjuser, elLjUser2; // elements var userName; // strings elLjuser = embeds[embed_index]; if(elLjuser.nodeName != "SPAN") { DebugMessage("Element frame does not look like a user reference, exiting"); continue; } userName = elLjuser.getAttribute(ATTR_LJUSER); // creating an HTML fragment to replace the broken link elLjUser2 = document.createElement("lj"); elLjUser2.setAttribute("user", userName); // replacing the element element.replaceChild(elLjUser2, elLjuser) embed_index--; } DebugMessage("FixUsers end"); return element; } // Fixes YouTube links // Takes a cloned node to fix // Returns it as well function FixYoutube(element) { var embeds; // list of embed objects DebugMessage("FixYoutube begin"); embeds = element.getElementsByClassName(CLS_EMBED); for (var embed_index=0; embed_index<embeds.length; embed_index++) { var elFrame, elLinkDiv, elLink, elRange, elFragment, elFirstNode; // elements var linkAddress, linkBodyNew; // strings elFrame = embeds[embed_index]; if(elFrame.nodeName != "DIV") { DebugMessage("Element frame does not look like a YouTube, exiting"); continue; } elLinkDiv = elFrame.nextSibling; if(elLinkDiv === null) { DebugMessage("Failed to find YouTube link DIV, exiting"); continue; } elLink = elLinkDiv.getElementsByTagName("a")[0]; if(elLink === null) { DebugMessage("YouTube link seems to be broken, exiting"); continue; } linkAddress = parse(REGEX_YOUTUBE_LINK, elLink.href, "YouTube Link"); linkBodyNew = EMBED_YOUTUBE_TEMPLATE .replace("{videoID}", linkAddress); // creating an HTML fragment to replace the broken link elRange = document.createRange(); elRange.selectNode(document.body); // required in Safari elFragment = elRange.createContextualFragment(linkBodyNew); elFirstNode = elFragment.firstChild; // replacing the element element.removeChild(elFrame); element.replaceChild(elFirstNode, elLinkDiv) embed_index--; } DebugMessage("FixYoutube end"); return element; } // Adds Repost links to one entry // Додає посилання «Перепост» внизу допису function addRepostLink(entry) { var elFooter, elRepostLink, elRepostLinkLI; // elements var entryTitle, entryLink, userName, entryId; // check if entry is a DOM element if( !entry.tagName ) { return; } elFooter = entry.getElementsByClassName(CLS_ENTRY_LINKS)[0]; // must be < ul > elRepostLink = document.createElement("a"); elRepostLink.href = POST_URL + "?" + REPOST_PARAM +"=1"; elRepostLink.target = "_blank"; elRepostLink.addEventListener('click', function(event) { rememberPost(entry); /*event.stopPropagation(); event.preventDefault();*/ }, true); elRepostLink.text = REPOST_TEXT; elRepostLinkLI = document.createElement("li"); elRepostLinkLI.appendChild(elRepostLink); elFooter.appendChild(elRepostLinkLI); } // Fills up the "Post Entry" form // Заповнює форму «Створити допис» function populatePostForm() { DebugMessage("Populate Post begin"); var repostedUrl = getQueryVariable(REPOST_PARAM); if(! repostedUrl) { DebugMessage("Does not look like a repost, exiting"); return; } // continue var repostDataString, repostData; // JSON data var postBody; // strings var elSubject, elBody; // elements repostDataString = getRepostData(); DebugMessage("Repost data=" + repostDataString); repostData = JSON.parse(repostDataString); DebugMessage("Repost data parsed=" + JSON.stringify(repostData)); DebugMessage("Preparing postBody..."); postBody = ((repostData.community == "") ? REPOST_TEMPLATE : REPOST_TEMPLATE_COMMUNITY) .replace('{poster}', repostData.poster) .replace('{community}', repostData.community) .replace('{link}', repostData.link) .replace('{subj}', repostData.subj) .replace('{body}', repostData.body) ; DebugMessage("postBody=" + postBody); elBody = document.getElementById(ID_INPUT_BODY); DebugMessage("elBody was: " + elBody.value); elBody.value = postBody; DebugMessage("elBody became: " + elBody.value); elSubject = document.getElementById(ID_INPUT_SUBJECT); DebugMessage("elSubject was: " + elSubject.value); elSubject.value = repostData.subj; DebugMessage("elSubject became: " + elSubject.value); setRepostData(CONTENT_DEFAULT); DebugMessage("Populate Post end"); } var thisUrl = window.location.href; if(thisUrl.indexOf(POST_URL2) != -1) { populatePostForm(); } else { addRepostLinks(); } })();
...
Дата: Четвер, 29 Грудень 2016 21:02 (UTC)Не надоели в ЖЖ посты-близнецы?
...
Дата: Четвер, 29 Грудень 2016 21:12 (UTC)...
Дата: Четвер, 29 Грудень 2016 21:14 (UTC)логично, но все же слишком облегчать это не стоит, как показала практика...
On 29 December 2016 at 23:12, bytebuster - DW Comment < dw_null@dreamwidth.org> wrote:
...
Дата: Четвер, 29 Грудень 2016 23:15 (UTC)Увы, "сижу" на обожаемом мною Pale Moon :)
А в нем Greasmonkey не работает, несмотря на самое близкое родство Pale Moon с Firefox ((
Наивный вопрос и не менее наивная просьба - а нельзя ли что-либо придумать такое-этакое, чтобы встраивать видео ( с Ютуба, например ) в КОММЕНТЫ на Дриме?
...
Дата: П'ятниця, 30 Грудень 2016 03:48 (UTC)але можна запостити линк з img з 1м фреймом, себто юзер кликне на картинці і перейде до ютуба.
напр., відео https://www.youtube.com/watch?v=yKnIhCmPycw
його 1й фрейм http://img.youtube.com/vi/yKnIhCmPycw/0.jpg
у коментарі пишемо
результат:
...
Дата: П'ятниця, 30 Грудень 2016 06:14 (UTC)...
Дата: П'ятниця, 30 Грудень 2016 10:16 (UTC)Спасибо за совет!
Извините за мою непонятливость, - а где и как найти ссылку на готовый первый фрейм видео?
Заранее благодарен!
Сейчас ссылку на первый фрейм составил вручную.
...
Дата: П'ятниця, 30 Грудень 2016 10:19 (UTC)Еще раз благодарю!
...
Дата: П'ятниця, 30 Грудень 2016 10:31 (UTC)С наступающим Новым годом!
...
Дата: П'ятниця, 30 Грудень 2016 16:39 (UTC)...
Дата: П'ятниця, 30 Грудень 2016 20:54 (UTC)Я что-то до этого и не додумался ...
Попробую так и сделать :)
...
Дата: П'ятниця, 30 Грудень 2016 14:48 (UTC)Дякую вам, пане!
...
Дата: П'ятниця, 30 Грудень 2016 14:53 (UTC)До вас тоді два питання:
1. Працює? Перевіряли?
2. Якщо знаєтеся на JS, можете зробити code review, будь-ласочка? Бо я протягом минулих 11 років пишу лише backends, і до UI ваащє нездатний. Боюся, що міг залишити чи дірки у безпеці, чи ще щось погане.
...
Дата: П'ятниця, 30 Грудень 2016 21:25 (UTC)1. Я для Хрома использовал tampermonkey - это тоже приложение, полный аналог Greasemonkey. Поставил, прописал скрипт, но не работает. Тэйпманки нашёл две ошибки кода, но, поскольку я не кодер, то лечить не брался.
2. Нет, я не знаюсь на ДжаваСкрипте. Начинал как-то заниматься на курсах АйТиВиДиэН, но жизнь внесла коррективы, потому через пару занятий пришлось бросить :(
(Иногда с бесконечными учёбами чуешь, что мозг скрипит не как на пятом десятке, а на восьмом, хехехе)
...
Дата: П'ятниця, 30 Грудень 2016 21:31 (UTC)57. if( match == null ) {
(пишет Use '==='to compare with 'null')
и
200. var postBody =
(пишет 'postBody is already defined)
...
Дата: П'ятниця, 30 Грудень 2016 21:44 (UTC)...
Дата: П'ятниця, 30 Грудень 2016 21:53 (UTC)...
Дата: Вівторок, 18 Квітень 2017 14:15 (UTC)Український переклад слова fixed?
...
Дата: Вівторок, 18 Квітень 2017 14:32 (UTC)...
Дата: Субота, 31 Грудень 2016 22:00 (UTC)...
Дата: Субота, 31 Грудень 2016 22:04 (UTC)...
Дата: Субота, 31 Грудень 2016 22:44 (UTC)...
Дата: Вівторок, 18 Квітень 2017 14:54 (UTC)...
Дата: Вівторок, 18 Квітень 2017 15:06 (UTC)1. Технологія — XSL-FO.
Форматтер під неї — Apache FOP.
Високорівнева бібліотека контролів для книжкового видавництва (на основі XSL-FO) — DocBook.
2. Технологія — LaTeX. Ніякого XML, чисто Юнікс.
Ну, або я не зрозумів запитання. :)
...
Дата: Вівторок, 18 Квітень 2017 15:26 (UTC)...
Дата: Вівторок, 18 Квітень 2017 16:06 (UTC)Чи, знов-таки, може я не зрозумів запитання. :)
...
Дата: Вівторок, 18 Квітень 2017 16:30 (UTC)Радше форматер коду, ніж редактор. Загалом використовую протягом кількох минулих років XMLSpear (www.donkeydevelopment.com). Або, часом, Aptana Studio.
Але воно, XMLSpear, трохи дурне. Наприклад, при форматуванні коду розриває деякі елементи отак (наслідуючи відступи, звісно):
Або й так:
Себто цей формально правильний та валідний XML не враховує нюанси вживання конкретного формату і надалі в програмах для читання замість нормального для людей відображення тексту буде показувати так, як ті програми навчені їхніми авторами показувати або виправляти вже суто типографічні помилки (як от пробіл перед комою, що в другому прикладі).
...
Дата: Вівторок, 18 Квітень 2017 17:24 (UTC)...
Дата: Неділя, 9 Квітень 2017 22:19 (UTC)у меня скрипт без всякой диагностики не срабатывает - простооткрывает окно нового поста, но не заполняет ни поле текста, ни поле темы. Firefox у меня вер. 52.0.2 (32-бит)
Greasemonkey версии 3.10.
Нет ли идеи, что не так? Может ли быть критично, что Файрфокс (и г-манки) не английские, а локиализованные?
...
Дата: Понеділок, 10 Квітень 2017 07:17 (UTC)Насправді, я там діагностики ніякої не писав (це ж скрипт на колєнці).
Тисніть Shift+Ctrl+J, відкриється віконце Browser Console, а потім тиць мишкою у «Перепостити». У віконці буде помилка і рядок.
...
Дата: Понеділок, 10 Квітень 2017 11:57 (UTC)...
Дата: Понеділок, 10 Квітень 2017 12:37 (UTC)...
Дата: Вівторок, 18 Квітень 2017 12:28 (UTC)Щоправда, цей варіант примусово видасть нову сторінку незалежно від налаштувань.
...
Дата: Вівторок, 18 Квітень 2017 14:07 (UTC)Поясніть, будь ласка, оцю фразу:
> нова (бета) сторінка створення повідомлення не увіменена в налаштуваннях
Бо не найшов у налаштуваннях.
І ще не дуже ясно оце:
> примусово видасть нову сторінку
— це маєте на увазі, нове вікно у target=_blank чи що?
...
Дата: Вівторок, 18 Квітень 2017 14:22 (UTC)https://www.dreamwidth.org/beta
можна ввімкнути або вимкнути бета-фічі, серед яких - нова сторінка створення/редагування повідомлення. Наскільки я розумію, вмикання цієї фічі дає редирект з /update на /entry/new , якщо ж вона вимкнена, кожна з цих сторінок доступна окремо (і виглядають по-різному). Нова сторінка (/entry/new) доступна всім користувачам, але якщо ця опція не ввімкнена, то всі лінки ведуть на стару, тому, щоб потрапити на нову сторінку, потрібно вводити її адресу вручну.
...
Дата: Вівторок, 18 Квітень 2017 14:35 (UTC)Гляну, чи можна програмно визначити, яка опція увімкнена (ну, не хочеться лазити на /beta щоразу).
Ну або фіг з ним, з видом сторінки, погляну, чи там на старій сторінці імена контролів інші можуть бути.
...
Дата: Вівторок, 18 Квітень 2017 14:39 (UTC)...
Дата: Вівторок, 18 Квітень 2017 16:00 (UTC)Воно поки що кривеньке:
1. Неправильно відображається одразу; але якщо перемкнути на HTML, а потім на Rich Text, то все видно нормально
2. Не знаю, як заборонити FCKEditor'у перепитувати "restore draft?"
Як будуть ідеї — допилю, а поки що так.
...
Дата: Четвер, 31 Серпень 2017 22:06 (UTC)1. Щось не працює фікс. Якщо я вірно зрозумів, то на /update JSON не підтягується взагалі, лише на /entry/new. Якщо ввімкнено в налаштуваннях відповідну бета-фічу, спрацьовує редирект з першого на друге, тоді все нормально. Але якщо її вимкнути, допомагає лише правка "
const POST_URL = "https://www.dreamwidth.org/entry/new";
" Ну та то таке, дрібниця.2. А от у lj щось, схоже, знов змінили. От що відбувається на lg-hater.livejournal.com:
bytebuster.dreamwidth.org/DreamWidth Reposter:
rememberPost begin
bytebuster.dreamwidth.org/DreamWidth Reposter:
FixYoutube begin
bytebuster.dreamwidth.org/DreamWidth Reposter:
FixYoutube end
bytebuster.dreamwidth.org/DreamWidth Reposter:
FixUsers begin
NotFoundError: Node was not found DreamWidth_Reposter.user.js:218.
bytebuster.dreamwidth.org/DreamWidth Reposter:
Populate Post begin
bytebuster.dreamwidth.org/DreamWidth Reposter:
getRepostData begin
bytebuster.dreamwidth.org/DreamWidth Reposter:
Repost data=
JSON.parse: unexpected end of data at line 1 column 1 of the JSON data DreamWidth_Reposter.user.js:387:16
Тобто помилка виникає в
element.replaceChild(elLjUser2, elLjuser)
І масив не формується взагалі, звідти й наступна помилка. Усе, до чого я дотумкав, це додати перед проблемною строкою:
DebugMessage("elLjUser"+elLjUser+" elLjuser2"+elLjuser2);
На що отримав ось таке повідомлення:
ReferenceError: elLjUser is not defined
Щось мені підказує, що проблема десь у CLS_LJUSER, і тому змінна embeds пуста. Але у джаваскрипті я повний невіглас, то знайти, де собаку зарито насправді, не спромігся.
А на ibigdan.livejournal.com взагалі посилання репосту не додається:
bytebuster.dreamwidth.org/DreamWidth Reposter:
Adding Repost links begin...
elFooter is undefined DreamWidth_Reposter.user.js:172:3
Здогадуюся, що значення elFooter має витягуватися звідкись з CLS_LJ_ENTRY_LINKS, але підробиць вже ніяк не второпаю.
Подивіться, будь ласка, як матимете час.
...
Дата: П'ятниця, 1 Вересень 2017 00:05 (UTC)1. А погляньте на попередню. гілку коментарів — це те ж саме?
2. ЖЖ зіпсувалося, воно і так не завжди працювало. Різні скіни по-різному рендерили, воно працювало тільки на деяких. На моїй схемі працювало, але я бачив такі, де — ні.
І ще, воно по-різному працювало у стрічці новин і в окремих ЖЖурналах. Теж не виправлено.
Але вибачте, я закрив свій еккаунт ЖЖ і не допилюватиму цей код під ЖЖ, просто нема де тестуватися.
...
Дата: П'ятниця, 1 Вересень 2017 00:31 (UTC)Насправді, у ЖЖ там главна муйня у тому, що до різних схем треба по-різному робити пошук, я не придумав універсального методу, як це робити, а возитися з ланцюжками «якщо не так, то ось так, а потім ще спробувати от так» — тупо полінувався. :(