├── WhoColor ├── __init__.py ├── tests │ ├── __init__.py │ ├── test_parser.py │ ├── profile_parser.py │ ├── test_utils.py │ └── prepare_test_data.py ├── static │ └── whocolor │ │ ├── images │ │ ├── ajax-loader.gif │ │ ├── Clear_icon.svg │ │ ├── Speechbubbles_icon.svg │ │ ├── Article_icon.svg │ │ ├── UserAvatar.svg │ │ ├── Speechbubbles_icon_green.svg │ │ └── ArticleSearch.svg │ │ └── readme │ │ └── whocolorpreview.png ├── handler.py ├── special_markups.py ├── utils.py └── parser.py ├── .gitignore ├── MANIFEST.in ├── LICENSE.txt ├── README.rst ├── setup.py └── userscript └── whocolor.user.js /WhoColor/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /WhoColor/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .cache 3 | *.pyc 4 | -------------------------------------------------------------------------------- /WhoColor/static/whocolor/images/ajax-loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikiwho/WhoColor/HEAD/WhoColor/static/whocolor/images/ajax-loader.gif -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | # Include the image files 2 | recursive-include WhoColor/static/whocolor/images/ * 3 | recursive-include WhoColor/static/whocolor/scripts/ * -------------------------------------------------------------------------------- /WhoColor/static/whocolor/readme/whocolorpreview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikiwho/WhoColor/HEAD/WhoColor/static/whocolor/readme/whocolorpreview.png -------------------------------------------------------------------------------- /WhoColor/static/whocolor/images/Clear_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 8 | 9 | -------------------------------------------------------------------------------- /WhoColor/static/whocolor/images/Speechbubbles_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /WhoColor/static/whocolor/images/Article_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /WhoColor/static/whocolor/images/UserAvatar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /WhoColor/static/whocolor/images/Speechbubbles_icon_green.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /WhoColor/static/whocolor/images/ArticleSearch.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 9 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /WhoColor/tests/test_parser.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import pickle 3 | import os 4 | 5 | from WhoColor.parser import WikiMarkupParser 6 | 7 | 8 | class TestParser(unittest.TestCase): 9 | def test_parser(self): 10 | test_data_file_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'test_data.p') 11 | with open(test_data_file_path, 'rb') as f: 12 | test_data = pickle.load(f) 13 | 14 | for article, data in test_data.items(): 15 | p = WikiMarkupParser(data['rev_text'], data['tokens']) 16 | p.generate_extended_wiki_markup() 17 | 18 | # Some of the entries in tuple are out of order. Not sure why and hence sorting both based on author id 19 | p.present_editors = tuple(sorted(list(p.present_editors), key=lambda x: x[0])) 20 | data['present_editors'] = tuple(sorted(list(data['present_editors']), key=lambda x: x[0])) 21 | 22 | assert p.extended_wiki_text == data['extended_wiki_text'], article 23 | assert p.present_editors == data['present_editors'], article 24 | -------------------------------------------------------------------------------- /WhoColor/tests/profile_parser.py: -------------------------------------------------------------------------------- 1 | import pickle 2 | import cProfile 3 | import pstats 4 | import io 5 | 6 | from WhoColor.parser import WikiMarkupParser 7 | 8 | 9 | def profile_parser(test_data_file): 10 | """ 11 | Test data file is a pickle file which contains a dictionary with rev text and tokens. 12 | """ 13 | with open(test_data_file, 'rb') as f: 14 | test_data = pickle.load(f) 15 | # shrinked datasets - to check the validity quickly and fix the bugs 16 | # test_data = pickle.load(open('who_color_test_data_shrinked.p','rb')) 17 | 18 | pr = cProfile.Profile() 19 | pr.enable() 20 | 21 | print('Profiling the WhoColor parser') 22 | for article, data in test_data.items(): 23 | print(article) 24 | p = WikiMarkupParser(data['rev_text'], data['tokens']) 25 | # cProfile.run('p.generate_extended_wiki_markup()') 26 | # break 27 | p.generate_extended_wiki_markup() 28 | 29 | pr.disable() 30 | 31 | s = io.StringIO() 32 | sortby = 'cumulative' 33 | ps = pstats.Stats(pr, stream=s).sort_stats(sortby) 34 | ps.print_stats() 35 | print(s.getvalue()) 36 | 37 | # save the content into a file and 38 | -------------------------------------------------------------------------------- /WhoColor/tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from WhoColor.utils import WikiWhoRevContent 3 | 4 | 5 | class TestUtils(unittest.TestCase): 6 | def test_wikiwho_from_page_id(self): 7 | # Page ID of 'Selfie' 8 | page_id = 38956275 9 | language = 'en' 10 | ww_rev_content = WikiWhoRevContent(page_id=page_id, language=language) 11 | # test if request data is correct 12 | request_data = ww_rev_content._prepare_request() 13 | data = {'url': 'https://www.wikiwho.net/{}/api/v1.0.0-beta/rev_content/page_id/{}/'.format(language, page_id), 14 | 'params': {'o_rev_id': 'true', 'editor': 'true', 'token_id': 'false', 'out': 'true', 'in': 'true'}} 15 | assert request_data == data 16 | # check if no errors 17 | ww_rev_content.get_revisions_and_tokens() 18 | 19 | def test_wikiwho_from_page_title(self): 20 | page_title = 'Selfie' 21 | language = 'en' 22 | ww_rev_content = WikiWhoRevContent(page_title=page_title, language=language) 23 | # test if request data is correct 24 | request_data = ww_rev_content._prepare_request() 25 | data = {'url': 'https://www.wikiwho.net/{}/api/v1.0.0-beta/rev_content/{}/'.format(language, page_title), 26 | 'params': {'o_rev_id': 'true', 'editor': 'true', 'token_id': 'false', 'out': 'true', 'in': 'true'}} 27 | assert request_data == data 28 | # check if no errors 29 | ww_rev_content.get_revisions_and_tokens() 30 | 31 | def test_wikiwho_from_rev_id(self): 32 | # First revision of 'Selfie' 33 | page_title = 'Selfie' 34 | rev_id = 547645475 35 | language = 'en' 36 | ww_rev_content = WikiWhoRevContent(page_title=page_title, rev_id=rev_id, language=language) 37 | # test if request data is correct 38 | request_data = ww_rev_content._prepare_request() 39 | data = {'url': 'https://www.wikiwho.net/{}/api/v1.0.0-beta/rev_content/{}/{}/'.format(language, page_title, rev_id), 40 | 'params': {'o_rev_id': 'true', 'editor': 'true', 'token_id': 'false', 'out': 'true', 'in': 'true'}} 41 | assert request_data == data 42 | # check if no errors 43 | ww_rev_content.get_revisions_and_tokens() 44 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | See her for some resource licenses (the files in the server/static folder): 2 | 3 | https://commons.wikimedia.org/w/index.php?title=File:Clear_icon.svg&oldid=149014810 4 | https://commons.wikimedia.org/w/index.php?title=File:Article_icon.svg&oldid=148759157 5 | https://commons.wikimedia.org/w/index.php?title=File:ArticleSearch.svg&oldid=150569436 6 | https://commons.wikimedia.org/w/index.php?title=File:Speechbubbles_icon.svg&oldid=148758659 7 | By MGalloway (WMF) (Own work) [CC BY-SA 3.0 (http://creativecommons.org/licenses/by-sa/3.0)], via Wikimedia Commons 8 | 9 | https://commons.wikimedia.org/w/index.php?title=File:UserAvatar.svg&oldid=150569452 10 | By MGalloway (WMF) (Own work) [CC BY-SA 4.0 (http://creativecommons.org/licenses/by-sa/4.0)], via Wikimedia Commons 11 | 12 | https://commons.wikimedia.org/w/index.php?title=File:Speechbubbles_icon_green.svg&oldid=135292327 13 | By Chiefwei (Own work) [CC BY-SA 4.0 (http://creativecommons.org/licenses/by-sa/4.0)], via Wikimedia Commons 14 | 15 | http://www.ajaxload.info/ <-- The origin of the ajax-loader.gif (License: WTFPL, http://www.wtfpl.net/) 16 | 17 | 18 | 19 | Everything else is licensed as followed: 20 | 21 | The MIT License (MIT) 22 | 23 | Copyright (c) 2015 Felix Stadthaus 24 | 25 | Permission is hereby granted, free of charge, to any person obtaining a copy 26 | of this software and associated documentation files (the "Software"), to deal 27 | in the Software without restriction, including without limitation the rights 28 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 29 | copies of the Software, and to permit persons to whom the Software is 30 | furnished to do so, subject to the following conditions: 31 | 32 | The above copyright notice and this permission notice shall be included in all 33 | copies or substantial portions of the Software. 34 | 35 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 36 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 37 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 38 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 39 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 40 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 41 | SOFTWARE. 42 | 43 | -------------------------------------------------------------------------------- /WhoColor/handler.py: -------------------------------------------------------------------------------- 1 | from WhoColor.utils import WikipediaRevText, WikiWhoRevContent 2 | from WhoColor.parser import WikiMarkupParser 3 | 4 | 5 | class WhoColorHandler(object): 6 | """ 7 | Example handler to create WhoColor API response data. 8 | """ 9 | 10 | def __init__(self, page_title=None, page_id=None, rev_id=None, language='en'): 11 | self.page_id = page_id 12 | self.page_title = page_title 13 | self.rev_id = rev_id 14 | self.language = language 15 | 16 | def __enter__(self): 17 | return self 18 | 19 | def handle(self): 20 | # get rev wiki text from wp 21 | wp_rev_text_obj = WikipediaRevText(self.page_title, self.page_id, self.rev_id, self.language) 22 | # {'page_id': , 'namespace': , 'rev_id': , 'rev_text': } 23 | rev_data = wp_rev_text_obj.get_rev_wiki_text() 24 | if rev_data is None or 'error' in rev_data or "-1" in rev_data: 25 | raise Exception('Problem with getting rev wiki text from wp.') 26 | 27 | # if rev_data['namespace'] != 0: 28 | # raise Exception('Invalid namespace.') 29 | 30 | # get revision content (authorship data) 31 | ww_rev_content_obj = WikiWhoRevContent(page_id=rev_data['page_id'], 32 | rev_id=rev_data['rev_id'], 33 | language=self.language) 34 | # revisions {rev_id: [timestamp, parent_id, class_name/editor, editor_name]} 35 | # tokens [[conflict_score, str, o_rev_id, in, out, editor/class_name, age]] 36 | # biggest conflict score (int) 37 | 38 | revisions = ww_rev_content_obj.get_revisions_data() 39 | editor_names_dict = ww_rev_content_obj.get_editor_names(revisions) 40 | tokens, biggest_conflict_score = ww_rev_content_obj.get_tokens_data(revisions, editor_names_dict) 41 | 42 | # annotate authorship data to wiki text 43 | # if registered user, class name is editor id 44 | p = WikiMarkupParser(rev_data['rev_text'], tokens) 45 | p.generate_extended_wiki_markup() 46 | extended_html = wp_rev_text_obj.convert_wiki_text_to_html(p.extended_wiki_text) 47 | 48 | wikiwho_data = {'revisions': revisions, 49 | 'tokens': ww_rev_content_obj.convert_tokens_data(tokens), 50 | 'biggest_conflict_score': biggest_conflict_score} 51 | return extended_html, p.present_editors, wikiwho_data 52 | 53 | def __exit__(self, exc_type, exc_val, exc_tb): 54 | pass 55 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | 2 | Current notes 3 | =========== 4 | NOTE 1: This is a side project by `Faflo `_ . I currently do not have the resources to follow up on all issues with WhoColor. So **please feel free to submit pull requests** or tell someone you know that is knowledgable to submit them. I will comment on issues, and help with resolving them. Pull requests can be done on the userscript, which will then be included by all clients. You can also help make the underlying parsing and API-code better, which lives in the /WhoColor/ folder. This, we will carefully review and then deploy to the underlying API, which we also run. 5 | 6 | NOTE 2: There is an official Firefox extension by the WMF created in collaboration with us that is much more refined than the userscript regarding the UI and some other things - although only offering provenance/authorship view, not conflict/age/history. Depending on what you need, this might be more suited for you: https://addons.mozilla.org/en-US/firefox/addon/whowrotethat/ 7 | 8 | 9 | 10 | 11 | 12 | WhoColor 13 | ======== 14 | The WhoColor userscript colors Wikipedia articles based on by-word authorship, gives color-coded information on conflicts and age and provides per-word revision histories. 15 | 16 | Take a look at http://f-squared.org/whovisual/ for more information. 17 | 18 | .. image:: https://github.com/wikiwho/WhoColor/blob/dev/WhoColor/static/whocolor/readme/whocolorpreview.png 19 | 20 | Requirements and Installation 21 | ============================= 22 | 23 | `requests `_ package is required to get revision meta data and text from Wikipedia api. 24 | 25 | 26 | Install WhoColor package (server code) using ``pip``:: 27 | 28 | pip install WhoColor 29 | 30 | 31 | Install WhoColor user script using ``Tampermonkey`` or ``Greasemonkey`` for Chrome and Firefox: 32 | 33 | - First install one of: 34 | - `Tampermonkey `_ for Chrome 35 | - `Tampermonkey `_ for Firefox 36 | - `Greasemonkey `_ for Firefox 37 | - Open `user script `_ and click on ``Raw`` button on the top right 38 | - ``Tampermonkey`` or ``Greasemonkey`` will automatically detect the user script and ask to install it 39 | - Open an article in Wikipedia and now you should see the ``WhoColor`` to the left of the default "Read" tab in the head navigation of the article 40 | 41 | Known Issues 42 | ======= 43 | * Only works guaranteed with the default Mediawiki skin 44 | * Check out the other issues https://github.com/wikiwho/WhoColor/issues 45 | 46 | 47 | Contact 48 | ======= 49 | * Fabian Floeck: f.floeck+wikiwho[.]gmail.com 50 | 51 | License 52 | ======= 53 | This work is licensed under MIT (some assets have other licenses, more detailed information in the LICENSE file). 54 | 55 | 56 | **Developed at Karlsruhe Institute of Technology and GESIS - Leibniz Institute for the Social Sciences** 57 | -------------------------------------------------------------------------------- /WhoColor/tests/prepare_test_data.py: -------------------------------------------------------------------------------- 1 | import pickle 2 | from os.path import exists 3 | 4 | from WhoColor.utils import WikipediaRevText, WikiWhoRevContent 5 | from WhoColor.parser import WikiMarkupParser 6 | 7 | 8 | def prepare_test_data(save_to_file, test_articles=None): 9 | if not test_articles: 10 | test_articles = ['Amstrad CPC', 'Antarctica', 'Apollo 11', 'Armenian Genocide', 'Barack_Obama', 11 | 'Bioglass', 'Bothrops_jararaca', 'Chlorine', 'Circumcision', 'Communist Party of China', 12 | 'Democritus', 'Diana,_Princess_of_Wales', 'Encryption', 'Eritrean Defence Forces', 13 | 'European Free Trade Association', 'Evolution', 'Geography of El Salvador', 'Germany', 14 | 'Home and Away', 'Homeopathy', 'Iraq War', 'Islamophobia', 'Jack the Ripper', 'Jesus', 15 | 'KLM destinations', 'Lemur', 'Macedonians_(ethnic_group)', 'Muhammad', 'Newberg, Oregon', 16 | 'Race_and_intelligence', 'Rhapsody_on_a_Theme_of_Paganini', 'Robert Hues', "Saturn's_moons_in_fiction", 17 | 'Sergei Korolev', 'South_Western_main_line', 'Special Air Service', 'The_Holocaust', 'Toshitsugu_Takamatsu', 18 | 'Vladimir_Putin', 'Wernher_von_Braun'] 19 | 20 | if exists(save_to_file): 21 | with open(save_to_file, 'rb') as f: 22 | test_data = pickle.load(f) 23 | else: 24 | test_data = {} 25 | 26 | for title in test_articles: 27 | print(title, ' started ...') 28 | if title not in test_data: 29 | # get rev wiki text from wp 30 | wp_rev_text_obj = WikipediaRevText(page_title=title, language='en') 31 | # {'page_id': , 'namespace': , 'rev_id': , 'rev_text': } 32 | rev_data = wp_rev_text_obj.get_rev_wiki_text() 33 | 34 | # get revision content (authorship data) 35 | ww_rev_content_obj = WikiWhoRevContent(page_id=rev_data['page_id'], 36 | rev_id=rev_data['rev_id'], 37 | language='en') 38 | # revisions {rev_id: [timestamp, parent_id, class_name/editor, editor_name]} 39 | # tokens [[conflict_score, str, o_rev_id, in, out, editor/class_name, age]] 40 | # biggest conflict score (int) 41 | 42 | revisions = ww_rev_content_obj.get_revisions_data() 43 | editor_names_dict = ww_rev_content_obj.get_editor_names(revisions) 44 | tokens, biggest_conflict_score = ww_rev_content_obj.get_tokens_data(revisions, editor_names_dict) 45 | 46 | # annotate authorship data to wiki text 47 | # if registered user, class name is editor id 48 | p = WikiMarkupParser(rev_data['rev_text'], tokens) 49 | p.generate_extended_wiki_markup() 50 | extended_html = wp_rev_text_obj.convert_wiki_text_to_html(p.extended_wiki_text) 51 | 52 | test_data[title] = {'extended_html': extended_html, 53 | 'extended_wiki_text': p.extended_wiki_text, 54 | 'present_editors': p.present_editors, 55 | 'revisions': revisions, 56 | 'tokens': tokens, 57 | 'biggest_conflict_score': biggest_conflict_score, 58 | 'rev_text': rev_data['rev_text']} 59 | print(title, ' ended ...') 60 | with open(save_to_file, 'wb') as f: 61 | pickle.dump(test_data, f) 62 | 63 | 64 | def shrink_test_data(from_file, to_file): 65 | with open(from_file, 'rb') as f: 66 | test_data = pickle.load(f) 67 | 68 | test_data_shrink = {} 69 | i = 0 70 | limit = 10 71 | for title, data in test_data.items(): 72 | test_data_shrink[title] = data 73 | i += 1 74 | if i >= limit: 75 | break 76 | 77 | with open(to_file, 'wb') as f: 78 | pickle.dump(test_data_shrink, f) 79 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """A setuptools based setup module. 2 | See: 3 | https://packaging.python.org/en/latest/distributing.html 4 | https://github.com/pypa/sampleproject 5 | """ 6 | 7 | # Always prefer setuptools over distutils 8 | from setuptools import setup, find_packages 9 | # To use a consistent encoding 10 | from codecs import open 11 | from os import path 12 | 13 | here = path.abspath(path.dirname(__file__)) 14 | 15 | # Get the long description from the README file 16 | with open(path.join(here, 'README.rst'), encoding='utf-8') as f: 17 | long_description = f.read() 18 | 19 | setup( 20 | name='WhoColor', 21 | 22 | # Versions should comply with PEP440. For a discussion on single-sourcing 23 | # the version across setup.py and the project code, see 24 | # https://packaging.python.org/en/latest/single_source_version.html 25 | version='1.0.1', 26 | 27 | description='An algorithm to create an on-demand color-markup of the original authors of the text of any article ' 28 | 'on the (english) Wikipedia.', 29 | long_description=long_description, 30 | 31 | # The project's main homepage. 32 | url='https://github.com/wikiwho/whoCOLOR', 33 | 34 | # Author details 35 | # author='The Python Packaging Authority', 36 | # author_email='pypa-dev@googlegroups.com', 37 | 38 | # Choose your license 39 | license='MIT', 40 | 41 | # See https://pypi.python.org/pypi?%3Aaction=list_classifiers 42 | classifiers=[ 43 | # How mature is this project? Common values are 44 | # 3 - Alpha 45 | # 4 - Beta 46 | # 5 - Production/Stable 47 | # 'Development Status :: 5 - Stable', 48 | 49 | # Indicate who your project is intended for 50 | # 'Intended Audience :: Developers', 51 | # 'Topic :: Software Development :: Build Tools', 52 | 53 | # Pick your license as you wish (should match "license" above) 54 | 'License :: OSI Approved :: MIT License', 55 | 56 | # Specify the Python versions you support here. In particular, ensure 57 | # that you indicate whether you support Python 2, Python 3 or both. 58 | # 'Programming Language :: Python :: 2', 59 | 'Programming Language :: Python :: 2.7', 60 | 'Programming Language :: Python :: 3', 61 | 'Programming Language :: Python :: 3.3', 62 | 'Programming Language :: Python :: 3.4', 63 | 'Programming Language :: Python :: 3.5', 64 | ], 65 | 66 | # What does your project relate to? 67 | keywords='wikipedia revision token authorship color visualization', 68 | 69 | # You can just specify the packages manually here if your project is 70 | # simple. Or you can use find_packages(). 71 | packages=find_packages(exclude=['contrib', 'docs', 'tests']), 72 | 73 | # Alternatively, if you want to distribute just a my_module.py, uncomment 74 | # this: 75 | # py_modules=["my_module"], 76 | 77 | # List run-time dependencies here. These will be installed by pip when 78 | # your project is installed. For an analysis of "install_requires" vs pip's 79 | # requirements files see: 80 | # https://packaging.python.org/en/latest/requirements.html 81 | install_requires=['requests', 'python-dateutil'], 82 | 83 | # List additional groups of dependencies here (e.g. development 84 | # dependencies). You can install these using the following syntax, 85 | # for example: 86 | # $ pip install -e .[dev,test] 87 | # extras_require={ 88 | # 'dev': ['check-manifest'], 89 | # 'test': ['coverage'], 90 | # }, 91 | 92 | # If there are data files included in your packages that need to be 93 | # installed, specify them here. If using Python 2.6 or less, then these 94 | # have to be included in MANIFEST.in as well. 95 | include_package_data=True, 96 | # package_data={ 97 | # 'sample': ['package_data.dat'], 98 | # }, 99 | 100 | # Although 'package_data' is the preferred approach, in some case you may 101 | # need to place data files outside of your packages. See: 102 | # http://docs.python.org/3.4/distutils/setupscript.html#installing-additional-files # noqa 103 | # In this case, 'data_file' will be installed into '/my_data' 104 | # data_files=[('my_data', ['data/data_file'])], 105 | 106 | # To provide executable scripts, use entry points in preference to the 107 | # "scripts" keyword. Entry points provide cross-platform support and allow 108 | # pip to create the appropriate form of executable for the target platform. 109 | # entry_points={ 110 | # 'console_scripts': [ 111 | # 'sample=sample:main', 112 | # ], 113 | # }, 114 | ) 115 | -------------------------------------------------------------------------------- /WhoColor/special_markups.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | :Authors: 5 | Felix Stadthaus, 6 | Kenan Erdogan 7 | """ 8 | import re 9 | 10 | REGEX_HELPER_PATTERN = 'WIKICOLORLB' 11 | 12 | SPECIAL_MARKUPS = ( 13 | # Internal wiki links 14 | { 15 | 'type': 'block', 16 | 'start_regex': re.compile(r'\[\['), 17 | 'end_regex': re.compile(r'\]\]'), 18 | 'no_spans': False, 19 | 'no_jump': False, 20 | }, 21 | # External links 22 | { 23 | 'type': 'block', 24 | 'start_regex': re.compile(r'\['), 25 | 'end_regex': re.compile(r'\]'), 26 | 'no_spans': False, 27 | 'no_jump': False, 28 | }, 29 | # Template tags and similar 30 | { 31 | 'type': 'block', 32 | 'start_regex': re.compile(r'{{'), 33 | 'end_regex': re.compile(r'}}'), 34 | 'no_spans': True, # no span is added around this element, 35 | 'no_jump': False 36 | }, 37 | # Reference tags - only start ref tag and attributes. 38 | # Closing ref is detected by 'General HTML tag' regex 39 | # { 40 | # 'type': 'block', 41 | # 'start_regex': re.compile(r''), 43 | # 'no_spans': True, 44 | # 'no_jump': False 45 | # }, 46 | # single tag 47 | { 48 | 'type': 'single', 49 | 'start_regex': re.compile(r'()'), 50 | 'end_regex': None, 51 | 'no_spans': True, 52 | 'no_jump': True 53 | }, 54 | # Math, timeline, nowiki tags 55 | { 56 | 'type': 'block', 57 | 'start_regex': re.compile(r'<(math|timeline|nowiki)[^>]*>'), 58 | 'end_regex': re.compile(r''), 59 | 'no_spans': True, 60 | 'no_jump': False 61 | }, 62 | # General HTML tag - only for text between <(tag) and > (tag name and attributes) 63 | { 64 | # 'type': 'single', 65 | # 'start_regex': re.compile(r'<\/?(ref|blockquote|ul|li)[^>]*>'), 66 | # 'end_regex': None, 67 | # 'no_spans': True, 68 | # 'no_jump': True 69 | 'type': 'block', 70 | 'start_regex': re.compile(r'<\/?(ref|h1|h2|h3|h4|h5|h6|p|br|hr|!--|abbr|b|bdi|bdo|blockquote|cite|code|data|del|dfn|em|i|ins|kbd|mark|pre|q|ruby|rt|rp|s|samp|small|strong|sub|sup|time|u|var|wbr|dl|dt|dd|ol|ul|li|div|span|table|tr|td|th|caption)'), 71 | 'end_regex': re.compile(r'>'), 72 | 'no_spans': True, 73 | 'no_jump': False 74 | }, 75 | 76 | # Headings 77 | { 78 | 'type': 'single', 79 | 'start_regex': re.compile(r'(=+|;)'), 80 | 'end_regex': None, 81 | 'no_spans': True, 82 | 'no_jump': True 83 | }, 84 | # Lists and blocks 85 | { 86 | 'type': 'block', 87 | 'start_regex': re.compile(r'[\\*#\\:]*;'), 88 | 'end_regex': re.compile(r'\\:'), 89 | 'no_spans': True, 90 | 'no_jump': False 91 | }, 92 | { 93 | 'type': 'single', 94 | 'start_regex': re.compile(r'[\\*#:]+'), 95 | 'end_regex': None, 96 | 'no_spans': True, 97 | 'no_jump': True 98 | }, 99 | # Horizontal lines 100 | { 101 | 'type': 'single', 102 | 'start_regex': re.compile(r'-----*'), 103 | 'end_regex': None, 104 | 'no_spans': True, 105 | 'no_jump': True 106 | }, 107 | # Table formatting 108 | # { 109 | # 'type': 'single', 110 | # 'start_regex': re.compile(r'(?<={})({\\||\\|}|\\|-|\\|\\+|\\|\\||)'.format(REGEX_HELPER_PATTERN)), 111 | # 'end_regex': None, 112 | # 'no_spans': True, 113 | # 'no_jump': True 114 | # }, 115 | { 116 | 'type': 'block', 117 | 'start_regex': re.compile(r'{\|'), 118 | 'end_regex': re.compile(r'\|}'), 119 | 'no_spans': True, 120 | 'no_jump': False 121 | }, 122 | # Linebreaks 123 | { 124 | 'type': 'single', 125 | 'start_regex': re.compile(r'({})+'.format(REGEX_HELPER_PATTERN)), 126 | 'end_regex': None, 127 | 'no_spans': True, 128 | 'no_jump': True 129 | }, 130 | # HTML Escape Sequences 131 | { 132 | 'type': 'single', 133 | 'start_regex': re.compile(r'( |€|"|&|<|>| |&(?:[a-z\d]+|#\d+|#x[a-f\d]+);)'), 134 | 'end_regex': None, 135 | 'no_spans': True, 136 | 'no_jump': True 137 | }, 138 | # Magic words 139 | { 140 | 'type': 'single', 141 | 'start_regex': re.compile(r'__(NOTOC|FORCETOC|TOC|NOEDITSECTION|NEWSECTIONLINK|NONEWSECTIONLINK|NOGALLERY|' 142 | r'HIDDENCAT|NOCONTENTCONVERT|NOCC|NOTITLECONVERT|NOTC|START|END|INDEX|NOINDEX|' 143 | r'STATICREDIRECT|DISAMBIG)__'), 144 | 'end_regex': None, 145 | 'no_spans': True, 146 | 'no_jump': True 147 | }, 148 | # Apostrophes for formatting 149 | { 150 | 'type': 'single', 151 | 'start_regex': re.compile(r'\'\'+'), 152 | 'end_regex': None, 153 | 'no_spans': True, 154 | 'no_jump': True 155 | } 156 | ) 157 | -------------------------------------------------------------------------------- /WhoColor/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | :Authors: 5 | Kenan Erdogan 6 | """ 7 | import requests 8 | import hashlib 9 | from dateutil import parser 10 | from datetime import datetime 11 | 12 | 13 | class WikipediaRevText(object): 14 | """ 15 | Example usage: 16 | wp_rev_text_obj = WikipediaRevText(page_title='Cologne', page_id=6187) 17 | # to get rev wiki text from wp api 18 | rev_data = wp_rev_text_obj.get_rev_wiki_text() 19 | # to convert (extended) wiki text into html by using wp api 20 | rev_extended_html = wp_rev_text_obj.convert_wiki_text_to_html(wiki_text=rev_data['rev_text']) 21 | """ 22 | 23 | def __init__(self, page_title=None, page_id=None, rev_id=None, language='en'): 24 | """ 25 | :param page_title: Title of an article. 26 | :param page_id: ID of an article 27 | :param rev_id: Revision id to get wiki text. 28 | :param language: Language of the page. 29 | """ 30 | self.page_id = page_id 31 | self.page_title = page_title 32 | self.rev_id = rev_id 33 | self.language = language 34 | 35 | def _prepare_request(self, wiki_text=None): 36 | data = {'url': 'https://{}.wikipedia.org/w/api.php'.format(self.language)} 37 | if wiki_text is None: 38 | params = {'action': 'query', 'prop': 'revisions', 39 | 'rvprop': 'content|ids', 'rvlimit': '1', 'format': 'json'} 40 | if self.page_id: 41 | params.update({'pageids': self.page_id}) 42 | elif self.page_title: 43 | params.update({'titles': self.page_title}) 44 | if self.rev_id is not None: 45 | params.update({'rvstartid': self.rev_id}) # , 'rvendid': rev_id}) 46 | else: 47 | params = {'action': 'parse', 'title': self.page_title, 48 | 'format': 'json', 'text': wiki_text, 'prop': 'text'} 49 | data['data'] = params 50 | return data 51 | 52 | def _make_request(self, data): 53 | response = requests.post(**data).json() 54 | return response 55 | 56 | def get_rev_wiki_text(self): 57 | """ 58 | If no rev id is given, text of latest revision is returned. 59 | If both article id and title are given, id is used in query. 60 | """ 61 | if self.page_id is None and self.page_title is None: 62 | raise Exception('Please provide id or title of the article.') 63 | 64 | data = self._prepare_request() 65 | response = self._make_request(data) 66 | 67 | if 'error' in response: 68 | return response 69 | pages = response['query']['pages'] 70 | if '-1' in pages: 71 | return pages 72 | for page_id, page in response['query']['pages'].items(): 73 | namespace = page['ns'] 74 | revisions = page.get('revisions') 75 | if revisions is None: 76 | return None 77 | else: 78 | return { 79 | 'page_id': int(page_id), 80 | 'namespace': namespace, 81 | 'rev_id': revisions[0]['revid'], 82 | 'rev_text': revisions[0]['*'] 83 | } 84 | 85 | def convert_wiki_text_to_html(self, wiki_text): 86 | """ 87 | Title of the article is required. 88 | """ 89 | if self.page_title is None: 90 | raise Exception('Please provide title of the article.') 91 | 92 | data = self._prepare_request(wiki_text) 93 | response = self._make_request(data) 94 | 95 | if 'error' in response: 96 | return response 97 | 98 | return response['parse']['text']['*'] 99 | 100 | 101 | class WikipediaUser(object): 102 | """ 103 | Example usage to get names of given editor ids: 104 | editor_ids = set(('30764272', '1465', '5959')) 105 | wp_user_obj = WikipediaUser() 106 | editors = wp_user_obj.get_editor_names(editor_ids) 107 | """ 108 | def __init__(self, language='en'): 109 | self.language = language 110 | 111 | def _prepare_request(self, editor_ids): 112 | params = {'action': 'query', 'list': 'users', 113 | 'format': 'json', 'ususerids': '|'.join(editor_ids)} 114 | return { 115 | 'url': 'https://{}.wikipedia.org/w/api.php'.format(self.language), 116 | 'data': params 117 | } 118 | 119 | def _make_request(self, data): 120 | response = requests.post(**data).json() 121 | return response 122 | 123 | def get_editor_names(self, editor_ids, batch_size=50): 124 | """ 125 | :param editor_ids: list of editor ids 126 | :param batch_size: number of editor ids (ususerids) in the query. WP allows 50 if not logged in. 127 | :return: a dict {editor_id: editor_name} 128 | """ 129 | editor_ids = list(editor_ids) 130 | editor_names = {} # {editor_id: editor_name, ..} 131 | editors_len = len(editor_ids) 132 | 133 | c = 1 134 | while True: 135 | data = self._prepare_request(editor_ids[batch_size*(c-1):batch_size*c]) 136 | response = self._make_request(data) 137 | 138 | if 'error' in response: 139 | return response 140 | users = response['query']['users'] 141 | if '-1' in users: 142 | return users 143 | 144 | for user in users: 145 | editor_names[str(user['userid'])] = user.get('name', None) 146 | 147 | if batch_size*c >= editors_len: 148 | break 149 | c += 1 150 | return editor_names 151 | 152 | 153 | class WikiWhoRevContent(object): 154 | """ 155 | Example usage: 156 | ww_rev_content_obj = WikiWhoRevContent(page_id=6187) 157 | wikiwho_data = ww_rev_content_obj.get_revisions_and_tokens() 158 | """ 159 | def __init__(self, page_id=None, page_title=None, rev_id=None, language='en'): 160 | self.page_id = page_id 161 | self.page_title = page_title 162 | self.rev_id = rev_id 163 | self.language = language 164 | 165 | def _prepare_request(self, rev_ids=False): 166 | ww_api_url = 'https://www.wikiwho.net/{}/api/v1.0.0-beta'.format(self.language) 167 | if rev_ids: 168 | if self.page_id: 169 | url_params = 'page_id/{}'.format(self.page_id) 170 | elif self.page_title: 171 | url_params = '{}'.format(self.page_title) 172 | return {'url': '{}/rev_ids/{}/'.format(ww_api_url, url_params), 173 | 'params': {'editor': 'true', 'timestamp': 'true'}} 174 | else: 175 | if self.page_id: 176 | url_params = 'page_id/{}'.format(self.page_id) 177 | elif self.rev_id: 178 | url_params = '{}/{}'.format(self.page_title, self.rev_id) 179 | elif self.page_title: 180 | url_params = '{}'.format(self.page_title) 181 | return {'url': '{}/rev_content/{}/'.format(ww_api_url, url_params), 182 | 'params': {'o_rev_id': 'true', 'editor': 'true', 183 | 'token_id': 'false', 'out': 'true', 'in': 'true'}} 184 | 185 | def _make_request(self, data): 186 | response = requests.get(**data).json() 187 | return response 188 | 189 | def get_revisions_data(self): 190 | # get revisions-editors 191 | data = self._prepare_request(rev_ids=True) 192 | response = self._make_request(data) 193 | # {rev_id: [timestamp, parent_id, editor]} 194 | revisions = {response['revisions'][0]['id']: [response['revisions'][0]['timestamp'], 195 | 0, 196 | response['revisions'][0]['editor']]} 197 | for i, rev in enumerate(response['revisions'][1:]): 198 | revisions[rev['id']] = [rev['timestamp'], 199 | response['revisions'][i]['id'], # parent = previous rev id 200 | rev['editor']] 201 | return revisions 202 | 203 | def get_editor_names(self, revisions): 204 | # get editor names from wp api 205 | editor_ids = {rev_data[2] for rev_id, rev_data in revisions.items() 206 | if rev_data[2] and not rev_data[2].startswith('0|')} 207 | wp_users_obj = WikipediaUser(self.language) 208 | editor_names_dict = wp_users_obj.get_editor_names(editor_ids) 209 | 210 | # extend revisions data 211 | # {rev_id: [timestamp, parent_id, class_name/editor, editor_name]} 212 | for rev_id, rev_data in revisions.items(): 213 | rev_data.append(editor_names_dict.get(rev_data[2], rev_data[2])) 214 | if rev_data[2].startswith('0|'): 215 | rev_data[2] = hashlib.md5(rev_data[2].encode('utf-8')).hexdigest() 216 | 217 | return editor_names_dict 218 | 219 | def get_tokens_data(self, revisions, editor_names_dict): 220 | data = self._prepare_request() 221 | response = self._make_request(data) 222 | _, rev_data = response['revisions'][0].popitem() 223 | tokens = rev_data['tokens'] 224 | 225 | # set editor and class names and calculate conflict score for each token 226 | # if registered user, class name is editor id 227 | biggest_conflict_score = 0 228 | for token in tokens: 229 | # set editor name 230 | token['editor_name'] = editor_names_dict.get(token['editor'], token['editor']) 231 | # set html class name 232 | if token['editor'].startswith('0|'): 233 | token['class_name'] = hashlib.md5(token['editor'].encode('utf-8')).hexdigest() 234 | else: 235 | token['class_name'] = token['editor'] 236 | # calculate age 237 | o_rev_ts = parser.parse(revisions[token['o_rev_id']][0]) 238 | age = datetime.now(o_rev_ts.tzinfo) - o_rev_ts 239 | token['age'] = age.total_seconds() 240 | # calculate conflict score 241 | editor_in_prev = None 242 | conflict_score = 0 243 | for i, out_ in enumerate(token['out']): 244 | editor_out = revisions[out_][2] 245 | if editor_in_prev is not None and editor_in_prev != editor_out: 246 | # exclude first deletions and self reverts (undo actions) 247 | conflict_score += 1 248 | try: 249 | in_ = token['in'][i] 250 | except IndexError: 251 | # no in for this out. end of loop. 252 | pass 253 | else: 254 | editor_in = revisions[in_][2] 255 | if editor_out != editor_in: 256 | # exclude self reverts (undo actions) 257 | conflict_score += 1 258 | editor_in_prev = editor_in 259 | token['conflict_score'] = conflict_score 260 | if conflict_score > biggest_conflict_score: 261 | biggest_conflict_score = conflict_score 262 | 263 | return tokens, biggest_conflict_score 264 | 265 | def convert_tokens_data(self, tokens): 266 | # convert into list. exclude unnecessary token data 267 | # [[conflict_score, str, o_rev_id, in, out, editor/class_name, age]] 268 | return [[token['conflict_score'], token['str'], token['o_rev_id'], 269 | token['in'], token['out'], token['class_name'], token['age']] 270 | for token in tokens] 271 | 272 | def get_revisions_and_tokens(self): 273 | """ 274 | Returns all revisions data of the article and tokens of given article. 275 | If no rev id is given, tokens of latest revision is returned. 276 | """ 277 | if self.page_id is None and self.page_title is None and self.rev_id is None: 278 | raise Exception('Please provide page id or page title or rev id.') 279 | 280 | revisions = self.get_revisions_data() 281 | editor_names_dict = self.get_editor_names(revisions) 282 | tokens, biggest_conflict_score = self.get_tokens_data(revisions, editor_names_dict) 283 | tokens = self.convert_tokens_data(tokens) 284 | 285 | return {'revisions': revisions, 286 | 'tokens': tokens, 287 | 'biggest_conflict_score': biggest_conflict_score} 288 | -------------------------------------------------------------------------------- /WhoColor/parser.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | :Authors: 5 | Felix Stadthaus, 6 | Kenan Erdogan 7 | """ 8 | import re 9 | import io 10 | from .special_markups import SPECIAL_MARKUPS, REGEX_HELPER_PATTERN 11 | 12 | 13 | class WikiMarkupParser(object): 14 | def __init__(self, wiki_text, tokens): # , revisions): 15 | # Saves the full wikipedia markup and all WikiWho tokens 16 | self.wiki_text = wiki_text 17 | self.tokens = tokens 18 | self.tokens_len = len(tokens) 19 | # self.revisions = revisions 20 | self.token = None 21 | # self.next_special_elem = None 22 | 23 | # Saves the current positions 24 | self._token_index = 0 25 | self._wiki_text_pos = 0 26 | 27 | # Saves whether there is currently an open span tag 28 | self._open_span = False 29 | # Array that holds the starting positions of blocks (special elements) we already jumped into 30 | self._jumped_elems = set() 31 | 32 | # The return values of the parser (error can be an error description) 33 | self.extended_wiki_text = io.StringIO() 34 | 35 | self.error = False 36 | self.present_editors = dict() # {editor_id: [editor_name, class_name, count], } 37 | self.conflict_scores = list() 38 | 39 | def __set_token(self): 40 | self.token = None 41 | if self._token_index < self.tokens_len: 42 | self.token = self.tokens[self._token_index] 43 | self.conflict_scores.append(self.token['conflict_score']) 44 | token_pos = re.search(re.escape(self.token['str']), self.wiki_text[self._wiki_text_pos:], re.IGNORECASE) 45 | if token_pos is None: 46 | # token is not found. because most probably it contains some characters that has different length 47 | # in lower and upper case such as 'İstanbul' 48 | # get next token 49 | self._token_index += 1 50 | self.__set_token() 51 | return 52 | self.token['end'] = self._wiki_text_pos + token_pos.end() 53 | if self.token['editor'] in self.present_editors: 54 | self.present_editors[self.token['editor']][2] += 1 55 | else: 56 | self.present_editors[self.token['editor']] = [self.token['editor_name'], 57 | self.token['class_name'], 58 | 1] # token count 59 | 60 | def __get_first_regex(self, regex): 61 | # first_match = None 62 | # for match in regex.finditer(self.wiki_text): 63 | # # search every time from the beginning of text 64 | # if (first_match is None or first_match.start() > match.start()) and match.start() >= self._wiki_text_pos: 65 | # # if match is first and starts after wiki text pos 66 | # first_match = match 67 | # if first_match is not None: 68 | # print('__get_first_regex:', self._wiki_text_pos, self._jumped_elems, first_match.start(), first_match.group(), regex) 69 | # return {'str': first_match.group(), 'start': first_match.start()} 70 | # return None 71 | # NOTE this doesnt work because if regex contains positive look behind! 72 | match = regex.search(self.wiki_text[self._wiki_text_pos:]) 73 | if match: 74 | return { 75 | 'str': match.group(), 76 | 'start': self._wiki_text_pos + match.start() 77 | } 78 | return None 79 | 80 | def __get_special_elem_end(self, special_elem): 81 | # Get end position of current special markup element 82 | end_pos_data = {} 83 | if special_elem.get('end_len') is not None and special_elem.get('end') is not None: 84 | # if special markup is single (has no end regex) 85 | end_pos_data['start'] = special_elem['end'] 86 | end_pos_data['len'] = special_elem['end_len'] 87 | end_pos_data['end'] = end_pos_data['start'] + end_pos_data['len'] 88 | else: 89 | end_regex = self.__get_first_regex(special_elem['end_regex']) 90 | if end_regex is not None: 91 | end_pos_data['start'] = end_regex['start'] 92 | end_pos_data['len'] = len(end_regex['str']) 93 | end_pos_data['end'] = end_pos_data['start'] + end_pos_data['len'] 94 | return end_pos_data 95 | 96 | def __get_next_special_element(self): 97 | # if self.next_special_elem and self.next_special_elem['start'] > self._wiki_text_pos: 98 | # return self.next_special_elem 99 | # Get starting position of next special markup element 100 | next_ = {} 101 | for special_markup in SPECIAL_MARKUPS: 102 | found_markup = self.__get_first_regex(special_markup['start_regex']) 103 | if found_markup is not None and \ 104 | (not next_ or next_['start'] > found_markup['start']) and \ 105 | found_markup['start'] not in self._jumped_elems: 106 | next_ = special_markup 107 | next_['start'] = found_markup['start'] 108 | next_['start_len'] = len(found_markup['str']) 109 | if next_['type'] == 'single': 110 | # to be used in __get_special_elem_end - because it has no end regex 111 | next_['end'] = next_['start'] 112 | next_['end_len'] = next_['start_len'] 113 | # self.next_special_elem = next_ 114 | return next_ 115 | 116 | def __add_spans(self, token, new_span=True): 117 | """ 118 | If there is an opened span and new_span is True, close previous span and start new span (no_spans=False) 119 | If there is an opened span and new_span is False, close previous span (no_spans=True) 120 | If there is not an opened span and new_span is True, start a new span (no_spans=False) 121 | If there is not an opened span and new_span is do nothing (no_spans=True) 122 | """ 123 | if self._open_span is True: 124 | self.extended_wiki_text.write('') 125 | self._open_span = False 126 | if new_span is True: 127 | self.extended_wiki_text.write(''.\ 128 | format(token['class_name'], self._token_index)) 129 | self._open_span = True 130 | 131 | def __parse_wiki_text(self, add_spans=True, special_elem=None, no_jump=False): 132 | """ 133 | There are 3 possible calls of this method in this algorithm. 134 | 1) start of script and adding not special tokens with spans around into extended markup: 135 | add_spans is True, special_elem is None and no_jump is False: Start, add spans around tokens until reaching 136 | next special element. And jump into that element and process tokens inside that element. 137 | 2) Handling special markup elements: 138 | add_spans is False, special_elem is not None and no_jump is True: Add each token until end of current special 139 | element into extended wiki text without spans. 140 | add_spans is False, special_elem is not None and no_jump is False: Add each token until end of current special 141 | element into extended wiki text without spans. If there is special element inside current special element, 142 | jump into that element and process tokens inside that element 143 | 144 | :param add_spans: Flag to decide adding spans around tokens. 145 | :param special_elem: Current special element that parser is inside. 146 | :param no_jump: Flag to decide jumping into next special element. 147 | :return: True if parsing is successful. 148 | """ 149 | # Get end position of current special markup 150 | special_elem_end = self.__get_special_elem_end(special_elem) if special_elem else False 151 | if no_jump is False: 152 | # Get starting position of next special markup element in wiki text 153 | next_special_elem = self.__get_next_special_element() 154 | 155 | while self._wiki_text_pos < (len(self.wiki_text) - 1): 156 | if self.token is None: 157 | # No token left to parse 158 | # Add everything that's left to the end of the extended wiki text 159 | self.extended_wiki_text.write(self.wiki_text[self._wiki_text_pos: len(self.wiki_text)]) 160 | self._wiki_text_pos = len(self.wiki_text) # - 1 161 | return True 162 | 163 | # Don't jump anywhere if no_jump is set 164 | if no_jump is False and (not special_elem_end or self._wiki_text_pos < special_elem_end['start']): 165 | if next_special_elem and \ 166 | (not special_elem_end or next_special_elem['start'] < special_elem_end['start']) and \ 167 | next_special_elem['start'] < self.token['end']: 168 | # Special markup element was found before or reaching into token 169 | # Or token itself is a start of special markup 170 | self._jumped_elems.add(next_special_elem['start']) 171 | 172 | if add_spans: 173 | # if no_spans=False, this special markup will have one span around with editor of first token 174 | self.__add_spans(self.token, new_span=not next_special_elem['no_spans']) 175 | 176 | # NOTE: add_spans=False => no spans will added inside special markups 177 | self.__parse_wiki_text(add_spans=False, 178 | special_elem=next_special_elem, 179 | no_jump=next_special_elem['no_jump']) 180 | if special_elem: 181 | # _wiki_text_pos is updated, we have to update the end position of current special markup 182 | special_elem_end = self.__get_special_elem_end(special_elem) 183 | # Get starting position of next special markup element 184 | next_special_elem = self.__get_next_special_element() 185 | continue 186 | 187 | # Is it end of special element? 188 | if special_elem_end and special_elem_end['end'] < self.token['end']: 189 | # Special element has been matched before the token 190 | # => Set position to special element's end 191 | self.extended_wiki_text.write(self.wiki_text[self._wiki_text_pos:special_elem_end['end']]) 192 | self._wiki_text_pos = special_elem_end['end'] 193 | return True 194 | 195 | # Add sequence author tags around token 196 | if add_spans: 197 | self.__add_spans(self.token) # close and open span tag 198 | 199 | # add remaining token (and possible preceding chars) to resulting altered markup 200 | self.extended_wiki_text.write(self.wiki_text[self._wiki_text_pos:self.token['end']]) 201 | self._wiki_text_pos = self.token['end'] 202 | 203 | # Increase token index 204 | self._token_index += 1 205 | # Get new token 206 | self.__set_token() 207 | 208 | # Close opened tags 209 | if self._open_span: 210 | self.extended_wiki_text.write("") 211 | self._open_span = False 212 | return True 213 | 214 | def __parse(self): 215 | # Current WikiWho token 216 | self.__set_token() 217 | return self.__parse_wiki_text() 218 | 219 | def __set_present_editors(self): 220 | """ 221 | Sort editors who owns tokens in given revision according to percentage of owned tokens in decreasing order. 222 | """ 223 | self.present_editors = tuple( 224 | (editor_name, class_name, token_count*100.0/self.tokens_len) 225 | for editor_id, (editor_name, class_name, token_count) in 226 | sorted(self.present_editors.items(), key=lambda x: x[1][2], reverse=True) 227 | ) 228 | 229 | def generate_extended_wiki_markup(self): 230 | """ 231 | Parse wiki text and add spans around tokens if possible. 232 | Generate list of editors who are present in this article page including 3 information to be used in js code: 233 | editor name, class name and authorship scores. 234 | """ 235 | # Add regex helper pattern into wiki text in order to keep newlines 236 | self.wiki_text = self.wiki_text.replace('\r\n', REGEX_HELPER_PATTERN).\ 237 | replace('\n', REGEX_HELPER_PATTERN).\ 238 | replace('\r', REGEX_HELPER_PATTERN) 239 | 240 | self.__parse() 241 | self.__set_present_editors() 242 | 243 | # Remove regex patterns 244 | self.wiki_text = self.wiki_text.replace(REGEX_HELPER_PATTERN, '\n') 245 | self.extended_wiki_text = self.extended_wiki_text.getvalue() 246 | self.extended_wiki_text = self.extended_wiki_text.replace(REGEX_HELPER_PATTERN, '\n') 247 | -------------------------------------------------------------------------------- /userscript/whocolor.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name WhoColor Userscript 3 | // @namespace https://wikiwho-api.wmcloud.org/whocolor/v1.0.0-beta/ 4 | // @version 1.1 5 | // @description Displays authorship information on wikipedia. 6 | // @require https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js 7 | // @require https://wikiwho-api.wmcloud.org/static/whocolor/scripts/moment-with-locales.js 8 | // @grant GM_addStyle 9 | // @grant GM_setValue 10 | // @grant GM_getValue 11 | // @grant GM_getResourceText 12 | // @grant GM_log 13 | // @include /^http(s?):\/\/(en|es|eu|de|tr).wikipedia.org\/(.+)/ 14 | // @copyright 2015+, Felix Stadthaus 15 | // ==/UserScript== 16 | 17 | // The MIT License (MIT) 18 | // 19 | // Copyright (c) 2015 Felix Stadthaus 20 | // 21 | // Permission is hereby granted, free of charge, to any person obtaining a copy 22 | // of this software and associated documentation files (the "Software"), to deal 23 | // in the Software without restriction, including without limitation the rights 24 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 25 | // copies of the Software, and to permit persons to whom the Software is 26 | // furnished to do so, subject to the following conditions: 27 | // 28 | // The above copyright notice and this permission notice shall be included in 29 | // all copies or substantial portions of the Software. 30 | // 31 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 32 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 33 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 34 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 35 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 36 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 37 | // THE SOFTWARE. 38 | 39 | // See here for some resource licenses: 40 | // 41 | // https://commons.wikimedia.org/w/index.php?title=File:Clear_icon.svg&oldid=149014810 42 | // https://commons.wikimedia.org/w/index.php?title=File:Article_icon.svg&oldid=148759157 43 | // https://commons.wikimedia.org/w/index.php?title=File:ArticleSearch.svg&oldid=150569436 44 | // https://commons.wikimedia.org/w/index.php?title=File:Speechbubbles_icon.svg&oldid=148758659 45 | // By MGalloway (WMF) (Own work) [CC BY-SA 3.0 (http://creativecommons.org/licenses/by-sa/3.0)], via Wikimedia Commons 46 | // 47 | // https://commons.wikimedia.org/w/index.php?title=File:UserAvatar.svg&oldid=150569452 48 | // By MGalloway (WMF) (Own work) [CC BY-SA 4.0 (http://creativecommons.org/licenses/by-sa/4.0)], via Wikimedia Commons 49 | // 50 | // https://commons.wikimedia.org/w/index.php?title=File:Speechbubbles_icon_green.svg&oldid=135292327 51 | // By Chiefwei (Own work) [CC BY-SA 4.0 (http://creativecommons.org/licenses/by-sa/4.0)], via Wikimedia Commons 52 | // 53 | // http://www.ajaxload.info/ <-- The origin of ajax-loader.gif (License: WTFPL, http://www.wtfpl.net/) 54 | 55 | // Pollyfill for Greasemonkey 4, taken from 56 | // https://github.com/greasemonkey/gm4-polyfill/blob/master/gm4-polyfill.js#L33 57 | if (typeof GM_addStyle == 'undefined') { 58 | this.GM_addStyle = (aCss) => { 59 | 'use strict'; 60 | let head = document.getElementsByTagName('head')[0]; 61 | if (head) { 62 | let style = document.createElement('style'); 63 | style.setAttribute('type', 'text/css'); 64 | style.textContent = aCss; 65 | head.appendChild(style); 66 | return style; 67 | } 68 | return null; 69 | }; 70 | } 71 | 72 | Wikiwho = { 73 | /* A few configuration options */ 74 | // Where to fetch Wikicolor data from 75 | wikicolorUrl: "https://wikiwho-api.wmcloud.org/", 76 | // wikicolorUrl: "https://www.wikiwho.net/", 77 | // wikicolorUrl: "http://127.0.0.1:8000/", 78 | 79 | // Color palette for highlighting of tokens (Kelly) 80 | tokenColors: [ 81 | "#FFB300", "#803E75", "#FF6800", "#A6BDD7", "#C10020", "#CEA262", "#817066", 82 | // The following is not good for people with defective color vision 83 | "#007D34", "#F6768E", "#00538A", "#FF7A5C", "#53377A", "#FF8E00", "#B32851", 84 | "#F4C800", "#7F180D", "#93AA00", "#593315", "#F13A13", "#232C16" 85 | ], 86 | 87 | /* Other initial values */ 88 | // Specifies whether the original, unaltered content is shown 89 | showingUnalteredContent: true, 90 | // True, when initialized has already been called to prevent double initialization 91 | initialized: false, 92 | // Array with colored authors (and the colors they are colored with) 93 | coloredAuthors: {}, 94 | // Variable holding the timeout that deselects the currently selected author 95 | deselectTimeout: null, 96 | // Variable telling whether provenance or history or conflict or age view view is opened 97 | provenanceViewOpen: false, 98 | historyViewOpen: false, 99 | conflictViewOpen: false, 100 | ageViewOpen: false, 101 | ageLimitFrom: 0, // days 102 | ageLimitTo: 360, // days 103 | groupSize: 10, 104 | 105 | /* Methods */ 106 | 107 | // Determines whether white or black are the more contrasting colors (via the YIQ color model) 108 | getContrastingColor: function(color){ 109 | var red = parseInt(color.substr(1, 2), 16); 110 | var green = parseInt(color.substr(3, 2), 16); 111 | var blue = parseInt(color.substr(5, 2), 16); 112 | var yiq = (299*red + 587*green + 114*blue) / 1000; 113 | return (yiq >= 128) ? ['black', 'darkblue'] : ['white', 'lightblue']; 114 | }, 115 | 116 | // Determines whether white or black are the more contrasting colors (via the YIQ color model) 117 | /* 118 | getContrastingColorRGB: function(red, green, blue){ 119 | var yiq = (299*red + 587*green + 114*blue) / 1000; 120 | return (yiq >= 128) ? ['black', 'darkblue'] : ['white', 'lightblue']; 121 | }, 122 | */ 123 | 124 | // Creates basic HTML elements like menu bars or buttons etc. 125 | createHTMLElements: function() { 126 | // Holds the altered Wikimedia content HTML markup 127 | Wikiwho.newcontent = $("
"); 128 | Wikiwho.newcontent.css('background', 'url('+Wikiwho.wikicolorUrl+'static/whocolor/images/ajax-loader.gif) no-repeat center center').css('min-height', '32px'); 129 | 130 | // Holds the original, unaltered Wikimedia content HTML markup 131 | Wikiwho.originalcontent = $("#mw-content-text"); 132 | 133 | // Add the altered content to the page (but hide it) 134 | $(Wikiwho.originalcontent[0].attributes).each(function() { Wikiwho.newcontent.attr(this.nodeName, this.value); }); 135 | Wikiwho.originalcontent.after(Wikiwho.newcontent); 136 | Wikiwho.newcontent.hide(); 137 | 138 | // The button to switch into wikicolor mode 139 | Wikiwho.onoffbutton = $('
  • WhoColor
  • '); 140 | $("#p-views ul").prepend(Wikiwho.onoffbutton); 141 | Wikiwho.onoffbutton.find("a").click(function() { Wikiwho.onoffbuttonclick(); }); 142 | 143 | // The menu on the right (displaying authors) 144 | var elementTop = $('#content').offset().top + 1; 145 | 146 | Wikiwho.rightbar = $('
    ').hide().prependTo($("div#content.mw-body")); 147 | $("div#content.mw-body").css("position", "relative"); 148 | Wikiwho.rightbar.css("top", "5em"); 149 | Wikiwho.rightbar.css("right", "calc(-15.5em - 3px)"); 150 | 151 | Wikiwho.rightbarcontent = $('
    ').appendTo(Wikiwho.rightbar); 152 | 153 | $(window).scroll(function(){ 154 | if($(window).scrollTop() > elementTop){ 155 | $('#wikiwhorightbar').css('top', '0px'); 156 | } else { 157 | $('#wikiwhorightbar').css('top', (elementTop-$(window).scrollTop())+'px'); 158 | } 159 | }); 160 | 161 | $(window).scroll(); 162 | 163 | // Editor list 164 | $('').appendTo(Wikiwho.rightbarcontent); 165 | $('').appendTo(Wikiwho.rightbarcontent); 166 | $('').appendTo(Wikiwho.rightbarcontent); 167 | $('

    Editor List

    ').appendTo(Wikiwho.rightbarcontent); 168 | $('
      ').appendTo(Wikiwho.rightbarcontent); 169 | // var today = new Date(); 170 | // today.setMonth(today.getMonth() - Wikiwho.ageLimit); 171 | // $('
      ').appendTo(Wikiwho.rightbarcontent); 172 | $('
      '+ 173 | ''+ 174 | '

      '+ 175 | ''+ 176 | '

      '+ 177 | ''+ 178 | '

      '+ 179 | ''+ 180 | '
      ').appendTo(Wikiwho.rightbarcontent).hide(); 181 | 182 | // Provenance view open button click event 183 | $('#provenanceviewbutton').click(function() { 184 | if(!Wikiwho.provenanceViewOpen) { 185 | // if provenance view is closed, conflict or age view must be open 186 | if (Wikiwho.conflictViewOpen) { 187 | Wikiwho.closeConflictView(); 188 | } else if (Wikiwho.ageViewOpen) { 189 | Wikiwho.closeAgeView(); 190 | } 191 | } 192 | }); 193 | // Conflict view open button click event 194 | $('#conflictviewbutton').click(function() { 195 | if(Wikiwho.conflictViewOpen) { 196 | Wikiwho.closeConflictView(); 197 | } else { 198 | Wikiwho.openConflictView(); 199 | } 200 | }); 201 | // Age view open button click event 202 | $('#ageviewbutton').click(function() { 203 | if(Wikiwho.ageViewOpen) { 204 | Wikiwho.closeAgeView(); 205 | } else { 206 | Wikiwho.openAgeView(); 207 | } 208 | }); 209 | $('#ageLimitButton').click(function(){ 210 | Wikiwho.ageLimitFrom = $('#ageLimitFrom').val(); 211 | Wikiwho.ageLimitTo = $('#ageLimitTo').val(); 212 | Wikiwho.groupSize = $('#groupSize').val(); 213 | Wikiwho.openAgeView(); 214 | }); 215 | $('#ageLimitFrom, #ageLimitTo, #groupSize').on("keypress", function(e){ 216 | if (e.keyCode === 13) { 217 | Wikiwho.ageLimitFrom = $('#ageLimitFrom').val(); 218 | Wikiwho.ageLimitTo = $('#ageLimitTo').val(); 219 | Wikiwho.groupSize = $('#groupSize').val(); 220 | Wikiwho.openAgeView(); 221 | } 222 | }); 223 | 224 | // The sequence history 225 | Wikiwho.seqHistBox = $('
      ').hide().appendTo($("div#content.mw-body")); 226 | Wikiwho.seqHistBox.append(''); 227 | Wikiwho.seqHistBox.append('
      Selected text part is too long for the WhoColor token history
      '); 228 | Wikiwho.seqHistBox.append('
      All the selected wiki markup was added in the currently viewed revision.
      '); 229 | Wikiwho.seqHistBox.append(''); 230 | Wikiwho.histview = $('
      ').appendTo(Wikiwho.seqHistBox); 231 | }, 232 | 233 | onoffbuttonclick: function() { 234 | Wikiwho.onoffbutton.toggleClass("selected"); 235 | if(Wikiwho.showingUnalteredContent) { 236 | Wikiwho.showAlteredContent(); 237 | }else{ 238 | Wikiwho.showUnalteredContent(); 239 | } 240 | }, 241 | 242 | showAlteredContent: function() { 243 | // Don't do anything if already showing the altered content 244 | if(!Wikiwho.showingUnalteredContent) return true; 245 | 246 | // The actual content replacement (just visual for the sake of speed) 247 | Wikiwho.originalcontent.attr("id", "old-mw-content-text"); 248 | Wikiwho.newcontent.attr("id", "mw-content-text"); 249 | Wikiwho.originalcontent.fadeOut(250, function() { Wikiwho.originalcontent.hide(); Wikiwho.newcontent.fadeIn(250, function() { Wikiwho.newcontent.show(); }); }); 250 | //Wikiwho.newcontent.show(); 251 | //Wikiwho.originalcontent.attr("id", "old-mw-content-text").hide(); 252 | //Wikiwho.newcontent.attr("id", "mw-content-text"); 253 | 254 | // Squeeze main content a bit and show the bar to the right 255 | //$("div#content").css("margin-right", "251px"); 256 | Wikiwho.rightbar.animate({"right": "0px"}, 500, function() {}); 257 | $("div#content").animate({"margin-right": "15.5em"}, 500, function() {}); 258 | Wikiwho.rightbar.show(); 259 | 260 | // Change flag 261 | Wikiwho.showingUnalteredContent = false; 262 | $('#provenanceviewbutton').addClass('provenanceviewbuttonopen'); 263 | $('#conflictviewbutton').removeClass('conflictviewbuttonopen'); 264 | $('#ageviewbutton').removeClass('ageviewbuttonopen'); 265 | }, 266 | 267 | showUnalteredContent: function() { 268 | // Don't do anything if already showing the unaltered content 269 | if(Wikiwho.showingUnalteredContent) return true; 270 | 271 | // The actual content replacement (just visual for the sake of speed) 272 | //Wikiwho.originalcontent.show(); 273 | Wikiwho.newcontent.attr("id", "new-mw-content-text"); 274 | Wikiwho.originalcontent.attr("id", "mw-content-text"); 275 | Wikiwho.newcontent.fadeOut(250, function() { Wikiwho.newcontent.hide(); Wikiwho.originalcontent.fadeIn(250, function() { Wikiwho.originalcontent.show(); }); }); 276 | 277 | // Unsqueeze main content and hide the bar to the right 278 | //$("div#content").css("margin-right", ""); 279 | //$("div#content").animate({"margin-right": ""}, 500, function() { Wikiwho.rightbar.hide(); }); 280 | Wikiwho.rightbar.animate({"right": "-15.6em"}, 500, function() { Wikiwho.rightbar.hide(); }); 281 | $("div#content").animate({"margin-right": ""}, 500, function() {}); 282 | //Wikiwho.rightbar.hide(); 283 | 284 | // Change flag 285 | Wikiwho.showingUnalteredContent = true; 286 | }, 287 | 288 | // Restore the original Mediawiki content 289 | restoreMWContent: function() { 290 | Wikiwho.originalcontent.show(); 291 | Wikiwho.newcontent.hide(); 292 | }, 293 | 294 | // get Wikiwho author data via ajax 295 | getWikiwhoData: function() { 296 | if(!Wikiwho.tries) Wikiwho.tries = 0; 297 | Wikiwho.tries++; 298 | 299 | if(Wikiwho.tries > 3) { 300 | // Failed 3 times, stop trying 301 | // Check error and info messages 302 | if(typeof Wikiwho.api_info !== "undefined") { 303 | alert(Wikiwho.api_info); 304 | } else if (typeof Wikiwho.api_error !== "undefined") { 305 | alert(Wikiwho.api_error); 306 | } else { 307 | alert("Failed to retrieve valid WikiWho data."); 308 | } 309 | return; 310 | } 311 | 312 | var queryDict = {}; 313 | location.search.substr(1).split("&").forEach(function(item) {queryDict[item.split("=")[0]] = item.split("=")[1]}); 314 | 315 | // "title": $("h1#firstHeading").text() 316 | var wiki_lang = location.hostname.split('.')[0]; 317 | var ajax_url = Wikiwho.wikicolorUrl + wiki_lang + "/whocolor/v1.0.0-beta/" + encodeURIComponent($("h1#firstHeading").text().trim()) + "/"; 318 | if(queryDict["oldid"]) { 319 | ajax_url = ajax_url + queryDict["oldid"] + "/"; 320 | } 321 | jQuery.ajax({ 322 | url: ajax_url, 323 | method: 'GET', 324 | // data: data, 325 | dataType: "json", 326 | success: Wikiwho.wikiwhoDataCallback, 327 | error: function() { 328 | // Request failed, try again 329 | setTimeout(Wikiwho.getWikiwhoData, 5000); // 5 seconds 330 | return; 331 | } 332 | }); 333 | }, 334 | 335 | wikiwhoDataCallback: function(data) { 336 | Wikiwho.api_success = data.success; 337 | Wikiwho.api_info = data.info; 338 | Wikiwho.api_error = data.error; 339 | // Retry when no success 340 | if(Wikiwho.api_success !== true) { 341 | setTimeout(Wikiwho.getWikiwhoData, 5000); 342 | return; 343 | } 344 | 345 | // Add extended markup content 346 | Wikiwho.newcontent.append(data.extended_html); 347 | 348 | // Remove loading indicator 349 | Wikiwho.newcontent.css('background', '').css('min-height', ''); 350 | 351 | // Save author and revision data 352 | Wikiwho.present_editors = data.present_editors; // [[editor_name, editor/class_name, editor_score]] 353 | Wikiwho.tokens = data.tokens; // [[conflict_score, str, o_rev_id, in, out, editor/class_name, age]] 354 | Wikiwho.tokencount = Wikiwho.tokens.length; 355 | Wikiwho.revisions = data.revisions; // {rev_id: [timestamp, parent_id, class_name/editor, editor_name]} 356 | Wikiwho.biggest_conflict_score = parseInt(data.biggest_conflict_score); 357 | Wikiwho.rev_id = parseInt(data.rev_id); 358 | 359 | // Fill right panel with data 360 | Wikiwho.fillRightPanel(); 361 | 362 | // Add token events for WikiWho markup 363 | Wikiwho.addTokenEvents(); 364 | 365 | // Add history view events 366 | // Add code handling text selections of Wikitext 367 | Wikiwho.addSelectionEvents(); 368 | // Add code to make the history view work 369 | Wikiwho.addHistoryEvents(); 370 | 371 | // Handle image selection outlines 372 | $('span.editor-token').has("img").addClass("editor-token-image"); 373 | 374 | // Debug output (color tokens in alternating colors) 375 | // $(".editor-token").filter(":odd").css("background-color", "green"); 376 | // $(".editor-token").filter(":even").css("background-color", "yellow"); 377 | }, 378 | 379 | addHistoryEvents: function() { 380 | // Open history view when open indicator is clicked 381 | Wikiwho.seqHistBox.click(function() { 382 | // Check whether open indicator was clicked 383 | if($(this).hasClass("indicator") && (!$(this).hasClass("indicatortoolong")) && (!$(this).hasClass("indicatoronerev"))) { 384 | // Calculate height of marked text part 385 | var selectionHeight = Wikiwho.seqEndToken.offset().top + Wikiwho.seqEndToken.outerHeight(false) - Wikiwho.seqStartToken.offset().top; 386 | 387 | // Calculate optimal history view height 388 | var maxviewheight = $(window).height() - (selectionHeight + 20); 389 | 390 | // Check whether selected text is too long 391 | if((maxviewheight < $(window).height()/5) || (maxviewheight < 150)) { 392 | // OK, that's too much at once :( 393 | Wikiwho.seqHistBox.addClass("indicatortoolong"); 394 | return; 395 | } 396 | 397 | // Prepare open animation 398 | Wikiwho.histview.css("height", ""); 399 | Wikiwho.seqHistBox.css("max-height", Wikiwho.seqHistBox.height()+"px"); 400 | Wikiwho.seqHistBox.removeClass("indicator"); 401 | Wikiwho.seqHistBox.animate({"max-height": maxviewheight+"px"}, 500, function() { 402 | Wikiwho.seqHistBox.css("max-height", "calc(100% - "+(selectionHeight + 20)+"px)"); 403 | 404 | // Fix sizes 405 | $(window).resize(); 406 | }); 407 | 408 | // Mark history view as open 409 | Wikiwho.historyViewOpen = true; 410 | 411 | // Reset some variables 412 | Wikiwho.hvhiddenTokens = new Array(); 413 | Wikiwho.hvhiddenRevisions = new Array(); 414 | Wikiwho.hvhiddenTokenDummies = new Array(); 415 | 416 | // Remove body scrollbars 417 | $("body").css("overflow", "hidden"); 418 | 419 | // Scroll body to text passage 420 | $('html, body').animate({ 421 | scrollTop: Wikiwho.seqStartToken.offset().top - 10 422 | }, 500); 423 | 424 | // Get start and end ID 425 | var startTokenId = parseInt(Wikiwho.seqStartToken.attr("id").slice(6)); 426 | Wikiwho.startTokenId = startTokenId; 427 | var endTokenId = parseInt(Wikiwho.seqEndToken.attr("id").slice(6)); 428 | if(Wikiwho.selectionEndTokenId) { 429 | endTokenId = parseInt(Wikiwho.selectionEndTokenId) - 1; 430 | } 431 | Wikiwho.endTokenId = endTokenId; 432 | 433 | // Clear and reset history view 434 | Wikiwho.histview.empty(); 435 | var leftbox = $('
      ').appendTo(Wikiwho.histview); 436 | var middlebox = $('
      ').appendTo(Wikiwho.histview); 437 | var rightbox = $('
      ').appendTo(Wikiwho.histview); 438 | 439 | // Populate history view 440 | var revisionsById = {}; 441 | var revisionArr = new Array(); 442 | revisionArr.push(Wikiwho.rev_id); 443 | revisionsById[Wikiwho.rev_id] = moment.utc(Wikiwho.revisions[Wikiwho.rev_id][0]); // timestamp 444 | var compiledTokens = new Array(); 445 | 446 | for (i = startTokenId; i <= endTokenId; i++) { 447 | var token_data = Wikiwho.tokens[i]; 448 | var tokenrevlist = new Array(); 449 | 450 | // origin rev id 451 | var revid = parseInt(token_data[2]); 452 | if(!(revid in revisionsById)) { 453 | revisionArr.push(revid); 454 | revisionsById[revid] = moment.utc(Wikiwho.revisions[revid][0]); 455 | } 456 | tokenrevlist.push(revid); 457 | 458 | // re-inserts (ins) 459 | token_data[3].forEach(function(entry) { 460 | var revid = parseInt(entry); 461 | if(!(revid in revisionsById)) { 462 | revisionArr.push(revid); 463 | revisionsById[revid] = moment.utc(Wikiwho.revisions[revid][0]); 464 | } 465 | tokenrevlist.push(revid); 466 | }); 467 | 468 | // deletions (outs) 469 | token_data[4].forEach(function(entry) { 470 | var revid = parseInt(entry); 471 | if(!(revid in revisionsById)) { 472 | revisionArr.push(revid); 473 | revisionsById[revid] = moment.utc(Wikiwho.revisions[revid][0]); 474 | } 475 | tokenrevlist.push(revid); 476 | }); 477 | 478 | tokenrevlist.sort(function(a, b){ 479 | var aD = revisionsById[a]; 480 | var bD = revisionsById[b]; 481 | return aD>bD ? -1 : aDbD ? -1 : aD').appendTo(leftbox); 495 | 496 | // Show diff links 497 | var article_title = encodeURIComponent($("h1#firstHeading").text().trim()); 498 | if(i !== 0) { 499 | revinfoline.append( 500 | $('') 510 | ); 511 | } 512 | var updownarrow = $(''); 513 | $('').html("↕").appendTo(updownarrow); 522 | 523 | // Append date and time 524 | revinfoline.append($('
      ').text(revisionsById[revisionArr[i]].format('YYYY-MM-DD'))); 525 | 526 | // Append spacer 527 | revinfoline.append($('
      ')); 528 | 529 | // Append author 530 | var author_name = Wikiwho.revisions[revisionArr[i]][3].replace(/^0\|/, ''); 531 | revinfoline.append($('
      ').text(author_name).addClass("hvauthorid-"+Wikiwho.revisions[revisionArr[i]][2]).append($('
      '))); 532 | 533 | // Append distance to next revision in list 534 | if(i !== revisionArr.length - 1) { 535 | var datetimediff = $('
      '); 536 | revinfoline.append(datetimediff); 537 | datetimediff.append($('').text(revisionsById[revisionArr[i+1]].from(revisionsById[revisionArr[i]], true))); 538 | datetimediff.append(updownarrow); 539 | 540 | // Calculate distance in revisions 541 | // TODO: Make this more efficient (maybe restructure data sent from API?) 542 | var counter = 0; 543 | var iterrevid = revisionArr[i]; 544 | var targetid = revisionArr[i+1]; 545 | while(iterrevid !== targetid) { 546 | counter++; 547 | iterrevid = Wikiwho.revisions[iterrevid][1]; // parent_id 548 | } 549 | datetimediff.append($('').text(counter + (counter===1 ? " revision" : " revisions"))); 550 | } 551 | } 552 | 553 | var tokenheaders = $('
      ').appendTo(middlebox); 554 | var tokenbodies = $('
      ').appendTo(middlebox); 555 | 556 | for (i = startTokenId; i <= endTokenId; i++) { 557 | token_data = Wikiwho.tokens[i]; 558 | var htmltoken = $('').text(token_data[1]); 559 | htmltoken.addClass("editor-token token-editor-"+token_data[5]); 560 | htmltoken.addClass("editor-token-"+i); 561 | tokenheaders.append($('
      ').append(htmltoken)); 562 | var tokencol = $('
      ').appendTo(tokenbodies); 563 | var tokenwidth = htmltoken.parent().outerWidth(true); 564 | var tokenrevindex = 0; 565 | var tokeninarticle = true; 566 | for (var revindex = 0; revindex < revisionArr.length - 1; revindex++) { 567 | if(revisionArr[revindex] === compiledTokens[i-startTokenId][tokenrevindex]) { 568 | tokeninarticle = !tokeninarticle; 569 | tokenrevindex++; 570 | } 571 | tokencol.append($('
      ')); 572 | } 573 | } 574 | 575 | // Fix scrolling 576 | tokenheaders.bind('wheel', function(e){ 577 | var scrollTo = e.originalEvent.deltaX + tokenbodies.scrollLeft(); 578 | tokenbodies.scrollLeft(scrollTo); 579 | }); 580 | leftbox.bind('wheel', function(e){ 581 | var scrollTo = e.originalEvent.deltaY + tokenbodies.scrollTop(); 582 | tokenbodies.scrollTop(scrollTo); 583 | }); 584 | tokenbodies.scroll(function() { 585 | tokenheaders.scrollLeft($(this).scrollLeft()); 586 | leftbox.scrollTop($(this).scrollTop()); 587 | }); 588 | tokenheaders.append($('
      ')); 589 | 590 | // Add resizing events 591 | $(window).resize(function() { 592 | if(leftbox.get(0).scrollHeight >= Wikiwho.seqHistBox.height()) { 593 | Wikiwho.histview.css("height", "calc("+($(window).height() - (selectionHeight + 20))+"px - 2.75em - 1px)"); 594 | }else{ 595 | Wikiwho.histview.css("height", ""); 596 | } 597 | }); 598 | 599 | // Check for special case (only one revision in list) 600 | if(revisionArr.length === 1) { 601 | Wikiwho.seqHistBox.addClass("indicator indicatoronerev"); 602 | 603 | // Remove resizing events 604 | $(window).off("resize"); 605 | 606 | // Mark history view as closed 607 | Wikiwho.historyViewOpen = false; 608 | 609 | // Restore body scrollbars 610 | $("body").css("overflow", ""); 611 | } 612 | 613 | // Add author click events 614 | leftbox.find(".hvrevauthor").click(function() { 615 | var editor = $(this).attr('class').match(/hvauthorid-([a-f0-9]+)/)[1]; 616 | $("li#editor-"+editor).click(); 617 | return false; 618 | }); 619 | 620 | // Add author hover events 621 | leftbox.find(".hvrevauthor").hover(function(event) { 622 | // Mousein event handler 623 | var editor = $(this).attr('class').match(/hvauthorid-([a-f0-9]+)/)[1]; 624 | 625 | // Call the general hover handler 626 | Wikiwho.hoverToken(editor); 627 | }, function(event) { 628 | // Mouseout event handler 629 | Wikiwho.deselectTimeout = setTimeout(function(){ 630 | // Remove all selection markers 631 | $(".editor-token").removeClass("selected hvselected"); 632 | $(".hvrevauthor").removeClass("selected"); 633 | $("#wikiwhorightbar li").removeClass("selected"); 634 | }, 500); 635 | }); 636 | 637 | // Color tokens 638 | Object.keys(Wikiwho.coloredAuthors).forEach(function(editor) { 639 | var color = Wikiwho.coloredAuthors[editor]; 640 | var contrastColor = Wikiwho.getContrastingColor(color); 641 | $("span.token-editor-"+editor).css({"background-color": color, "color": contrastColor[0]}).find("*").css("color", contrastColor[1]); 642 | $("div.hvauthorid-"+editor).css({"background-color": color, "color": contrastColor[0]}); 643 | }); 644 | 645 | // Color tokens differently if conflict view open 646 | if(Wikiwho.conflictViewOpen) { 647 | Wikiwho.openConflictView(); 648 | } 649 | 650 | // Add hover events 651 | $('div.hvtokenheaders span.editor-token').hover(function(event) { 652 | // Mousein event handler 653 | var editor = $(this).attr('class').match(/token-editor-([a-f0-9]+)/)[1]; 654 | var tokenid = $(this).attr('id').slice(10); 655 | 656 | // Call the general hover handler 657 | Wikiwho.hoverToken(editor); 658 | 659 | // Select token with red outline 660 | // TODO is this correct? 661 | // $("#token-age-"+tokenid).removeClass("selected").addClass("hvselected"); 662 | $(this).removeClass("selected").addClass("hvselected"); 663 | }, function(event) { 664 | // Mouseout event handler 665 | Wikiwho.deselectTimeout = setTimeout(function(){ 666 | // Remove all selection markers 667 | $(".author-token").removeClass("selected hvselected"); 668 | $(".hvrevauthor").removeClass("selected"); 669 | $("#wikiwhorightbar li").removeClass("selected"); 670 | }, 500); 671 | }); 672 | 673 | // Add click events 674 | $('div.hvtokenheaders span.editor-token').click(function() { 675 | var editor = $(this).attr('class').match(/token-editor-([a-f0-9]+)/)[1]; 676 | $("#editor-"+editor).click(); 677 | return false; 678 | }); 679 | 680 | // Add hide events (rightclick) 681 | $('div.hvtokenheaders span.editor-token').bind("contextmenu",function(e){ 682 | var editor = $(this).attr('class').match(/token-editor-([a-f0-9]+)/)[1]; 683 | var tokenid = $(this).attr('id').slice(10); 684 | 685 | Wikiwho.hvHideToken(parseInt(tokenid), parseInt(editor), revisionArr, compiledTokens, revisionsById, true); 686 | 687 | return false; 688 | }); 689 | 690 | // Open history view 691 | Wikiwho.seqHistBox.animate({"max-height": maxviewheight+"px"}, 500, function() { 692 | Wikiwho.seqHistBox.css("max-height", "calc(100% - "+(selectionHeight + 20)+"px)"); 693 | 694 | // Fix sizes 695 | // $(window).resize(); 696 | }); 697 | } 698 | }); 699 | 700 | // Close history box when close icon is clicked 701 | $("img.hvcloseicon").click(function() { 702 | // Remove resizing events 703 | $(window).off("resize"); 704 | 705 | // Close animation 706 | Wikiwho.seqHistBox.css("max-height", Wikiwho.seqHistBox.outerHeight(false) + "px"); 707 | Wikiwho.seqHistBox.animate({"max-height": "0px"}, 500, function() { 708 | Wikiwho.seqHistBox.hide(); 709 | Wikiwho.seqHistBox.css("max-height", ""); 710 | // Restore body scrollbars 711 | $("body").css("overflow", ""); 712 | }); 713 | 714 | // Mark history view as closed 715 | Wikiwho.historyViewOpen = false; 716 | }); 717 | }, 718 | 719 | hvHideToken: function(tokenid, authorid, revisionArr, compiledTokens, revisionsById, animation) { 720 | Wikiwho.hvhiddenTokens.push(tokenid); 721 | 722 | // Search for already matching token dummies 723 | var foundDummies = new Array(); 724 | for(i = 0; i < Wikiwho.hvhiddenTokenDummies.length; i++) { 725 | var dummy = Wikiwho.hvhiddenTokenDummies[i]; 726 | 727 | // Check for removed dummies 728 | if(dummy === undefined) continue; 729 | 730 | if ((dummy["start"] === tokenid + 1) || (dummy["end"] === tokenid - 1)) { 731 | foundDummies.push(dummy); 732 | } 733 | } 734 | 735 | // Check how many matching dummies were found 736 | if(foundDummies.length === 0) { 737 | // No dummy matching, add one 738 | var newid = Wikiwho.hvhiddenTokenDummies.length; 739 | var htmltoken = $('').text("[...]"); 740 | var dummyobj = $('
      ').append(htmltoken).insertAfter($('div.hvtokenheaders span.editor-tokenid-'+tokenid).parent()); 741 | var tokencol = $('
      ').insertAfter($('div.hvtokenbodies div.hvtokencol-'+tokenid)); 742 | var tokenwidth = htmltoken.parent().outerWidth(true); 743 | for (var revindex = 0; revindex < revisionArr.length - 1; revindex++) { 744 | tokencol.append($('
      ')); 745 | } 746 | 747 | dummy = { 748 | 'start': tokenid, 749 | 'end': tokenid, 750 | 'object': dummyobj, 751 | 'colobj': tokencol, 752 | 'id': newid 753 | }; 754 | 755 | dummyobj.click(function() { 756 | Wikiwho.hvRestoreDummy(dummy, revisionArr, compiledTokens, revisionsById, true); 757 | }); 758 | 759 | Wikiwho.hvhiddenTokenDummies[newid] = dummy; 760 | 761 | // Animation 762 | var dummywidth = dummyobj.width(); 763 | var tokencolwidth = tokencol.width(); 764 | if(animation) { 765 | dummyobj.css('width', '0px'); 766 | tokencol.css('width', '0px'); 767 | dummyobj.animate({'width': dummywidth+'px'}, 500, function() { 768 | $(this).css("width", ""); 769 | }); 770 | tokencol.animate({'width': tokencolwidth+'px'}, 500, function() { 771 | $(this).css("width", ""); 772 | }); 773 | } 774 | }else if(foundDummies.length === 1) { 775 | // One dummy matching, add to dummy 776 | dummy = foundDummies[0]; 777 | var dummyid = dummy['id'] 778 | if(dummy['start'] === tokenid + 1) { 779 | dummy['start']--; 780 | }else{ 781 | dummy['end']++; 782 | } 783 | 784 | Wikiwho.hvhiddenTokenDummies[dummyid] = dummy; 785 | }else{ 786 | if(animation) { 787 | foundDummies[1]['object'].animate({'width': '0px'}, 500, function() { 788 | $(this).remove(); 789 | }); 790 | foundDummies[1]['colobj'].animate({'width': '0px'}, 500, function() { 791 | $(this).remove(); 792 | }); 793 | }else{ 794 | foundDummies[1]['object'].remove(); 795 | foundDummies[1]['colobj'].remove(); 796 | } 797 | if(foundDummies[0]['start'] > foundDummies[1]['start']) foundDummies[0]['start'] = foundDummies[1]['start']; 798 | if(foundDummies[0]['end'] < foundDummies[1]['end']) foundDummies[0]['end'] = foundDummies[1]['end']; 799 | Wikiwho.hvhiddenTokenDummies[foundDummies[1]['id']] = undefined; 800 | } 801 | 802 | // Actual hiding of token column 803 | if(animation) { 804 | $('.hvtokenhead-'+tokenid).animate({'width': '0px'}, 500, function() { 805 | $(this).hide(); 806 | $(this).css("width", ""); 807 | }); 808 | $('.hvtokencol-'+tokenid).animate({'width': '0px'}, 500, function() { 809 | $(this).hide(); 810 | $(this).css("width", ""); 811 | }); 812 | }else{ 813 | $('.hvtokenhead-'+tokenid).hide(); 814 | $('.hvtokencol-'+tokenid).hide(); 815 | } 816 | 817 | // Rehide rows that should already be hidden (in case a new dummy was added) 818 | for(i = 0; i < Wikiwho.hvhiddenRevisions.length; i++) { 819 | $('.hvrevhead-'+Wikiwho.hvhiddenRevisions[i]).hide(); 820 | $('.hvtokencolpiece-'+Wikiwho.hvhiddenRevisions[i]).hide(); 821 | } 822 | 823 | // Check whether we can hide rows as well 824 | var newRevisionArray = new Array(); 825 | for(var i = 0; i < compiledTokens.length; i++) { 826 | // Skip token if hidden 827 | if(Wikiwho.hvhiddenTokens.indexOf(Wikiwho.startTokenId + i) !== -1) { 828 | continue; 829 | } 830 | // Add revisions of this token to array if not already in there 831 | for(var i2 = 0; i2 < compiledTokens[i].length; i2++) { 832 | if(newRevisionArray.indexOf(compiledTokens[i][i2]) === -1) { 833 | newRevisionArray.push(compiledTokens[i][i2]); 834 | } 835 | } 836 | } 837 | // Go through real revision array and hide all revisions that are not in the new revision array and not already hidden 838 | for(i = 1; i < revisionArr.length; i++) { 839 | if(newRevisionArray.indexOf(revisionArr[i]) === -1) { 840 | // Revision not in new revision array 841 | if(Wikiwho.hvhiddenRevisions.indexOf(revisionArr[i]) === -1) { 842 | // Not hidden yet, hide 843 | if(animation) { 844 | $('.hvrevhead-'+revisionArr[i]).animate({'height': '0px'}, 500, function() { 845 | $(this).hide(); 846 | $(this).css("height", ""); 847 | }); 848 | $('.hvtokencolpiece-'+revisionArr[i]).animate({'height': '0px'}, 500, function() { 849 | $(this).hide(); 850 | $(this).css("height", ""); 851 | }); 852 | }else{ 853 | $('.hvrevhead-'+revisionArr[i]).hide(); 854 | $('.hvtokencolpiece-'+revisionArr[i]).hide(); 855 | } 856 | 857 | // Get index of previous shown revision 858 | var previousRevIndex = i - 1; 859 | while(Wikiwho.hvhiddenRevisions.indexOf(revisionArr[previousRevIndex]) !== -1) { 860 | previousRevIndex--; 861 | } 862 | 863 | // Get index of next shown revision 864 | var nextRevIndex = i + 1; 865 | while((nextRevIndex < revisionArr.length) && (Wikiwho.hvhiddenRevisions.indexOf(revisionArr[nextRevIndex]) !== -1)) { 866 | nextRevIndex++; 867 | } 868 | if(nextRevIndex === revisionArr.length) { 869 | // Shouldn't show a diff 870 | // TODO 871 | }else{ 872 | // Calculate and update new date diff data of previous shown revision 873 | $('.hvrevhead-'+revisionArr[previousRevIndex]+' .hvlefttimediff').text(revisionsById[revisionArr[nextRevIndex]].from(revisionsById[revisionArr[previousRevIndex]], true)); 874 | 875 | // Calculate distance in revisions 876 | // TODO: Make this more efficient (maybe restructure data sent from API?) 877 | var counter = 0; 878 | var iterrevid = revisionArr[previousRevIndex]; 879 | var targetid = revisionArr[nextRevIndex]; 880 | while(iterrevid !== targetid) { 881 | counter++; 882 | iterrevid = Wikiwho.revisions[iterrevid][1]; // parent_id 883 | } 884 | 885 | // Update distance in revisions 886 | $('.hvrevhead-'+revisionArr[previousRevIndex]+' .hvrighttimediff').text(counter + (counter===1 ? " revision" : " revisions")); 887 | } 888 | 889 | // Add to hvhiddenRevisions array 890 | Wikiwho.hvhiddenRevisions.push(revisionArr[i]); 891 | } 892 | } 893 | } 894 | }, 895 | 896 | hvRestoreDummy: function(dummy, revisionArr, compiledTokens, revisionsById, animation) { 897 | // Remove dummy objects 898 | dummy['object'].remove(); 899 | dummy['colobj'].remove(); 900 | 901 | // Show token columns again 902 | for(i = dummy["start"]; i <= dummy["end"]; i++) { 903 | // Actual showing of token column 904 | var headwidth = $('.hvtokenhead-'+i).width(); 905 | var colwidth = $('.hvtokencol-'+i).width(); 906 | if(animation) { 907 | $('.hvtokenhead-'+i).css('width', '0px'); 908 | $('.hvtokencol-'+i).css('width', '0px'); 909 | } 910 | $('.hvtokenhead-'+i).show(); 911 | $('.hvtokencol-'+i).show(); 912 | if(animation) { 913 | $('.hvtokenhead-'+i).animate({'width': headwidth+'px'}, 500, function() { 914 | $(this).css("width", ""); 915 | }); 916 | $('.hvtokencol-'+i).animate({'width': colwidth+'px'}, 500, function() { 917 | $(this).css("width", ""); 918 | }); 919 | } 920 | 921 | // Remove tokens from hidden tokens array 922 | Wikiwho.hvhiddenTokens.splice(Wikiwho.hvhiddenTokens.indexOf(i), 1); 923 | } 924 | 925 | // Remove dummy from array 926 | Wikiwho.hvhiddenTokenDummies[dummy['id']] = undefined; 927 | 928 | // Check whether we can show rows as well 929 | var newRevisionArray = new Array(); 930 | for(i = 0; i < compiledTokens.length; i++) { 931 | // Skip token if hidden 932 | if(Wikiwho.hvhiddenTokens.indexOf(Wikiwho.startTokenId + i) !== -1) { 933 | continue; 934 | } 935 | // Add revisions of this token to array if not already in there 936 | for(i2 = 0; i2 < compiledTokens[i].length; i2++) { 937 | if(newRevisionArray.indexOf(compiledTokens[i][i2]) === -1) { 938 | newRevisionArray.push(compiledTokens[i][i2]); 939 | } 940 | } 941 | } 942 | // Go through real revision array and show all revisions that are in the new revision array and hidden 943 | for(i = 1; i < revisionArr.length; i++) { 944 | if(newRevisionArray.indexOf(revisionArr[i]) !== -1) { 945 | // Revision in new revision array 946 | if(Wikiwho.hvhiddenRevisions.indexOf(revisionArr[i]) !== -1) { 947 | // Is hidden => show 948 | if(animation) { 949 | $('.hvrevhead-'+revisionArr[i]).show().animate({'height': '4.5em'}, 500, function() { 950 | $(this).css("height", ""); 951 | }); 952 | $('.hvtokencolpiece-'+revisionArr[i]).show().animate({'height': '4.5em'}, 500, function() { 953 | $(this).css("height", ""); 954 | }); 955 | }else{ 956 | $('.hvrevhead-'+revisionArr[i]).show(); 957 | $('.hvtokencolpiece-'+revisionArr[i]).show(); 958 | } 959 | 960 | // Get index of previous shown revision 961 | var previousRevIndex = i - 1; 962 | while(Wikiwho.hvhiddenRevisions.indexOf(revisionArr[previousRevIndex]) !== -1) { 963 | previousRevIndex--; 964 | } 965 | 966 | // Get index of next shown revision 967 | var nextRevIndex = i + 1; 968 | while((nextRevIndex < revisionArr.length) && (Wikiwho.hvhiddenRevisions.indexOf(revisionArr[nextRevIndex]) !== -1)) { 969 | nextRevIndex++; 970 | } 971 | 972 | // Correct diff of previous revision 973 | // Calculate and update new date diff data of previous shown revision 974 | $('.hvrevhead-'+revisionArr[previousRevIndex]+' .hvlefttimediff').text(revisionsById[revisionArr[i]].from(revisionsById[revisionArr[previousRevIndex]], true)); 975 | 976 | // Calculate distance in revisions 977 | // TODO: Make this more efficient (maybe restructure data sent from API?) 978 | var counter = 0; 979 | var iterrevid = revisionArr[previousRevIndex]; 980 | var targetid = revisionArr[i]; 981 | while(iterrevid !== targetid) { 982 | counter++; 983 | iterrevid = Wikiwho.revisions[iterrevid][1]; // parent_id 984 | } 985 | 986 | // Update distance in revisions 987 | $('.hvrevhead-'+revisionArr[previousRevIndex]+' .hvrighttimediff').text(counter + (counter===1 ? " revision" : " revisions")); 988 | 989 | // Correct diff of this revision 990 | if(nextRevIndex === revisionArr.length) { 991 | // Shouldn't show a diff 992 | // TODO 993 | }else{ 994 | // Calculate and update new date diff data of this shown revision 995 | $('.hvrevhead-'+revisionArr[i]+' .hvlefttimediff').text(revisionsById[revisionArr[nextRevIndex]].from(revisionsById[revisionArr[i]], true)); 996 | 997 | // Calculate distance in revisions 998 | // TODO: Make this more efficient (maybe restructure data sent from API?) 999 | counter = 0; 1000 | iterrevid = revisionArr[i]; 1001 | targetid = revisionArr[nextRevIndex]; 1002 | while(iterrevid !== targetid) { 1003 | counter++; 1004 | iterrevid = Wikiwho.revisions[iterrevid][1]; // parent_id 1005 | } 1006 | 1007 | // Update distance in revisions 1008 | $('.hvrevhead-'+revisionArr[i]+' .hvrighttimediff').text(counter + (counter===1 ? " revision" : " revisions")); 1009 | } 1010 | 1011 | // Remove from hvhiddenRevisions array 1012 | Wikiwho.hvhiddenRevisions.splice(Wikiwho.hvhiddenRevisions.indexOf(revisionArr[i]), 1); 1013 | } 1014 | } 1015 | } 1016 | }, 1017 | 1018 | addSelectionEvents: function() { 1019 | $("html").mouseup(function(e) { 1020 | if (window.getSelection) { 1021 | // Cancel if history or age view is already opened 1022 | if(Wikiwho.historyViewOpen || Wikiwho.ageViewOpen) { 1023 | return; 1024 | } 1025 | 1026 | // Cancel if mouse is at open indicator / hist box 1027 | if(Wikiwho.seqHistBox.css("display") !== "none") { 1028 | var relX = e.pageX - Wikiwho.seqHistBox.offset().left; 1029 | var relY = e.pageY - Wikiwho.seqHistBox.offset().top; 1030 | if((relX >= 0) && (relY >= 0) && (relX < Wikiwho.seqHistBox.outerWidth()) && (relY < Wikiwho.seqHistBox.outerHeight())) { 1031 | return; 1032 | } 1033 | } 1034 | 1035 | selectionRange = window.getSelection().getRangeAt(0); 1036 | 1037 | // Check whether something is selected 1038 | if(!selectionRange.collapsed) { 1039 | // Set start and end container (should be spans) 1040 | var firstToken = $(selectionRange.startContainer.parentElement); 1041 | var lastToken = $(selectionRange.endContainer.parentElement); 1042 | 1043 | // Reset some variable 1044 | Wikiwho.selectionEndTokenId = undefined; 1045 | 1046 | // Don't do anything if we can't associate the selection with author-tokens 1047 | if(!firstToken.hasClass("editor-token")) { 1048 | var tempFirstToken = $(selectionRange.startContainer.nextElementSibling); 1049 | if(tempFirstToken.hasClass("editor-token")) { 1050 | firstToken = tempFirstToken 1051 | }else{ 1052 | tempFirstToken = firstToken.parent(); 1053 | if(tempFirstToken.hasClass("editor-token")) { 1054 | firstToken = tempFirstToken; 1055 | }else{ 1056 | return; 1057 | } 1058 | } 1059 | } 1060 | if(!lastToken.hasClass("editor-token")) { 1061 | var tempLastToken = $(selectionRange.endContainer.previousElementSibling); 1062 | if(tempLastToken.hasClass("editor-token")) { 1063 | lastToken = tempLastToken 1064 | }else{ 1065 | tempLastToken = lastToken.parent(); 1066 | if(tempLastToken.hasClass("editor-token")) { 1067 | lastToken = tempLastToken; 1068 | for(i = 0; i < 3; i++) { 1069 | if(tempLastToken.next().hasClass("editor-token")) { 1070 | Wikiwho.selectionEndTokenId = tempLastToken.next().attr("id").slice(6); 1071 | break; 1072 | } 1073 | if(tempLastToken.next().find("span.editor-token").length > 0) { 1074 | Wikiwho.selectionEndTokenId = tempLastToken.next().find("span.editor-token").first().attr("id").slice(6); 1075 | break; 1076 | } 1077 | tempLastToken = tempLastToken.parent(); 1078 | } 1079 | }else{ 1080 | return; 1081 | } 1082 | } 1083 | } 1084 | 1085 | 1086 | // Check whether these start and end tokens are already saved and indicator is shown 1087 | if(firstToken.is(Wikiwho.seqStartToken) && lastToken.is(Wikiwho.seqEndToken) && (Wikiwho.seqHistBox.css("display") !== "none")) { 1088 | // Cancel and don't reopen the indicator 1089 | return; 1090 | } 1091 | 1092 | // Save start and end token 1093 | Wikiwho.seqStartToken = firstToken; 1094 | Wikiwho.seqEndToken = lastToken; 1095 | 1096 | // Calculate height of marked text part 1097 | var selectionHeight = Wikiwho.seqEndToken.offset().top + Wikiwho.seqEndToken.outerHeight(false) - Wikiwho.seqStartToken.offset().top; 1098 | 1099 | // Calculate optimal history view height 1100 | var maxviewheight = $(window).height() - (selectionHeight + 20); 1101 | 1102 | // Stop hide animation (allows text selection via double-click) 1103 | Wikiwho.seqHistBox.stop(); 1104 | 1105 | // Check whether selection is too big and if so, notify the user 1106 | if((maxviewheight < $(window).height()/5) || (maxviewheight < 150)) { 1107 | Wikiwho.seqHistBox.addClass("indicator"); 1108 | Wikiwho.seqHistBox.addClass("indicatortoolong"); 1109 | Wikiwho.seqHistBox.css("bottom", "-2em"); 1110 | Wikiwho.seqHistBox.animate({"bottom": "0px"}, 300, function() {}); 1111 | Wikiwho.seqHistBox.show(); 1112 | return; 1113 | } 1114 | 1115 | // Show history view indicator 1116 | Wikiwho.seqHistBox.addClass("indicator"); 1117 | Wikiwho.seqHistBox.removeClass("indicatortoolong"); 1118 | Wikiwho.seqHistBox.removeClass("indicatoronerev"); 1119 | Wikiwho.seqHistBox.css("bottom", "-2em"); 1120 | Wikiwho.seqHistBox.animate({"bottom": "0px"}, 300, function() {}); 1121 | Wikiwho.seqHistBox.show(); 1122 | }else{ 1123 | // Hide history view indicator 1124 | if(!Wikiwho.historyViewOpen) { 1125 | Wikiwho.seqHistBox.animate({"bottom": "-2em"}, 300, function() { 1126 | Wikiwho.seqHistBox.hide(); 1127 | Wikiwho.seqHistBox.css("top", ""); 1128 | }); 1129 | } 1130 | } 1131 | } 1132 | }); 1133 | 1134 | Wikiwho.newcontent.mousedown(function() { 1135 | 1136 | }); 1137 | }, 1138 | 1139 | fillRightPanel: function() { 1140 | // Create list box for authors 1141 | var authorListBox = $("#wikiwhoAuthorList").empty(); 1142 | 1143 | // Add authors to list box 1144 | for (var i = 0; i < Wikiwho.present_editors.length; i++) { 1145 | var author_name = Wikiwho.present_editors[i][0]; 1146 | var author_id = Wikiwho.present_editors[i][1]; // class name 1147 | var author_score = Wikiwho.present_editors[i][2]; 1148 | var author_is_anonymous = author_name.startsWith('0|') 1149 | // console.log(author_id, author_name, author_score); 1150 | 1151 | author_name = author_name.replace(/^0\|/, '') 1152 | 1153 | var authentry = $('
    • '+author_score.toFixed(1)+'%
    • ').appendTo(authorListBox); 1154 | $('').appendTo(authentry); 1157 | $(''+author_name+'').appendTo(authentry); 1158 | 1159 | // Create click handler (wrap in a closure first so the variables are passed correctly) 1160 | (function(author_id, authentry) { 1161 | authentry.mousedown(function(e){ e.preventDefault(); }); 1162 | authentry.click(function() { 1163 | if(typeof Wikiwho.coloredAuthors[author_id] === 'undefined') { 1164 | // if editor is not selected already 1165 | if(Wikiwho.tokenColors.length === 0) { 1166 | alert("You can't select any more editors. Please deselect an editors first to be able to select another one again."); 1167 | return; 1168 | } 1169 | 1170 | if(Wikiwho.conflictViewOpen) { 1171 | alert("Conflict view is opened! Please close the conflict view first."); 1172 | return; 1173 | } else if(Wikiwho.ageViewOpen) { 1174 | alert("Age view is opened! Please close the age view first."); 1175 | return; 1176 | } 1177 | 1178 | //var colorindex = Math.floor(Math.random()*Wikiwho.tokenColors.length); 1179 | var color = Wikiwho.tokenColors.splice(0, 1)[0]; 1180 | var contrastColor = Wikiwho.getContrastingColor(color); 1181 | Wikiwho.coloredAuthors[author_id] = color; 1182 | $("span.token-editor-"+author_id).css({"background-color": color, 1183 | "color": contrastColor[0]}).find("*").css("color", contrastColor[1]); 1184 | $("div.hvauthorid-"+author_id).css({"background-color": color, "color": contrastColor[0]}); 1185 | authentry.css({"background-color": color, 1186 | "color": contrastColor[0]}); 1187 | }else{ 1188 | // if editor is already selected 1189 | Wikiwho.tokenColors.unshift(Wikiwho.coloredAuthors[author_id]); 1190 | delete Wikiwho.coloredAuthors[author_id]; 1191 | $("span.token-editor-"+author_id).css({"background-color": "", "color": ""}).find("*").css("color", ""); 1192 | $("div.hvauthorid-"+author_id).css({"background-color": "", "color": ""}); 1193 | authentry.css({"background-color": "", "color": ""}); 1194 | } 1195 | }); 1196 | 1197 | authentry.hover(function(event) { 1198 | // Mousein event handler 1199 | 1200 | // Remove all selection markers 1201 | $("span.editor-token").removeClass("selected hvselected"); 1202 | $("div.hvrevauthor").removeClass("selected"); 1203 | $("#wikiwhoAuthorList li").removeClass("selected"); 1204 | clearTimeout(Wikiwho.deselectTimeout); 1205 | 1206 | // Mark all tokens of this author 1207 | $("span.token-editor-"+author_id).addClass("selected"); 1208 | $("div.hvauthorid-"+author_id).addClass("selected"); 1209 | $("li#editor-"+author_id).addClass("selected"); 1210 | }, function(event) { 1211 | // Mouseout event handler 1212 | Wikiwho.deselectTimeout = setTimeout(function(){ 1213 | // Remove all selection markers 1214 | $("span.editor-token").removeClass("selected hvselected"); 1215 | $("div.hvrevauthor").removeClass("selected"); 1216 | $("#wikiwhoAuthorList li").removeClass("selected"); 1217 | }, 500); 1218 | }); 1219 | })(author_id, authentry); 1220 | } 1221 | }, 1222 | 1223 | hoverToken: function(authorid) { 1224 | // Clear deselect timeout 1225 | clearTimeout(Wikiwho.deselectTimeout); 1226 | 1227 | // Clear "current token" marker 1228 | $(".hvselected").removeClass("hvselected").addClass("selected"); 1229 | 1230 | // Clear hvauthor marker 1231 | $("div.hvrevauthor").removeClass("selected"); 1232 | 1233 | // Determine whether this author is already/still selected 1234 | var selected = $("#wikiwhoAuthorList li.selected"); 1235 | if(selected.length >= 1) { 1236 | var selectedAuthId = selected.attr('id').slice(7); 1237 | if(selectedAuthId === authorid) { 1238 | // Already selected, don't do anything else 1239 | return; 1240 | } 1241 | 1242 | selected.stop( false, true ).stop( false, true ).stop( false, true ); 1243 | selected.removeClass("selected"); 1244 | $("span.token-editor-"+selectedAuthId).removeClass("selected"); 1245 | } 1246 | 1247 | // Scroll the author list to the position of the current entrys author 1248 | Wikiwho.scrollToShowAuthEntry(authorid); 1249 | 1250 | // Mark all tokens of this author 1251 | $("span.token-editor-"+authorid).addClass("selected"); 1252 | $("div.hvauthorid-"+authorid).addClass("selected"); 1253 | $("li#editor-"+authorid).addClass("selected"); 1254 | 1255 | // Flash the author entry 1256 | $("li#editor-"+authorid).delay(300).fadeOut(100).fadeIn(300); 1257 | }, 1258 | 1259 | addTokenEvents: function() { 1260 | var authortokens = $("span.editor-token"); 1261 | 1262 | authortokens.hover(function(event) { 1263 | // Mousein event handler 1264 | var authorid = $(this).attr('class').match(/token-editor-([a-f0-9]+)/)[1]; 1265 | var tokenid = $(this).attr('id').slice(6); 1266 | 1267 | // Call the general hover handler 1268 | Wikiwho.hoverToken(authorid); 1269 | 1270 | // If history view is open, add red outline to current token 1271 | if((Wikiwho.historyViewOpen) && ($("span#token-age-"+tokenid).length === 1)) { 1272 | // Add outline 1273 | $("span#token-age-"+tokenid).removeClass("selected").addClass("hvselected"); 1274 | 1275 | // Scroll history view to right position if necessary 1276 | $("#wikiwhoseqhistbox .hvtokenbodies").stop(true); 1277 | var tokenleft = $("span#token-age-"+tokenid).parent().position().left; 1278 | var tokenright = tokenleft + $("span#token-age-"+tokenid).parent().outerWidth(); 1279 | var scrollpos = $("#wikiwhoseqhistbox .hvtokenbodies").scrollLeft(); 1280 | 1281 | if(tokenleft < 0) { 1282 | $("#wikiwhoseqhistbox .hvtokenbodies").stop(true).animate({scrollLeft: tokenleft+scrollpos}, 500); 1283 | }else if(tokenright > $("#wikiwhoseqhistbox .hvtokenbodies").width()-2) { 1284 | $("#wikiwhoseqhistbox .hvtokenbodies").stop(true).animate({scrollLeft: tokenright+scrollpos-$("#wikiwhoseqhistbox .hvtokenbodies").outerWidth()+2}, 500); 1285 | } 1286 | } 1287 | }, function(event) { 1288 | // Mouseout event handler 1289 | Wikiwho.deselectTimeout = setTimeout(function(){ 1290 | // Remove all selection markers 1291 | $("span.editor-token").removeClass("selected hvselected"); 1292 | $("div.hvrevauthor").removeClass("selected"); 1293 | $("#wikiwhoAuthorList li").removeClass("selected"); 1294 | }, 500); 1295 | }); 1296 | 1297 | authortokens.click(function() { 1298 | if(Wikiwho.conflictViewOpen) { 1299 | alert("Conflict view is opened! Please close the conflict view first."); 1300 | return; 1301 | } else if(Wikiwho.ageViewOpen) { 1302 | alert("Age view is opened! Please close the age view first."); 1303 | return; 1304 | } 1305 | var editor = $(this).attr('class').match(/token-editor-([a-f0-9]+)/)[1]; 1306 | $("li#editor-"+editor).click(); 1307 | return false; 1308 | }); 1309 | }, 1310 | 1311 | scrollToShowAuthEntry: function(editor) { 1312 | // Scroll target 1313 | var authEntry = $('li#editor-'+editor); 1314 | 1315 | // Don't try to scroll if there is no target to scroll to 1316 | if(authEntry.length === 0) return; 1317 | 1318 | // Set a few helper Variables 1319 | var authList = $('#wikiwhorightbar'); 1320 | var authListTop = authList.scrollTop(); 1321 | var listHeight = authList.height(); 1322 | var entryTop = authEntry.position().top; 1323 | var entryHeight = authEntry.height(); 1324 | 1325 | // Determine whether we have to scroll 1326 | if(entryTop < 0) { 1327 | // Entry is too high, scroll up 1328 | authList.stop().animate({ 1329 | scrollTop: entryTop + authListTop 1330 | }, 300); 1331 | }else if(entryTop > listHeight - entryHeight) { 1332 | // Entry is too low, scroll down 1333 | authList.stop().animate({ 1334 | scrollTop: entryTop + authListTop - listHeight + entryHeight 1335 | }, 300); 1336 | } 1337 | }, 1338 | 1339 | openConflictView: function() { 1340 | // Do nothing - no conflicts (special case) 1341 | if(Wikiwho.biggest_conflict_score === 0) { 1342 | alert('There is no conflict.'); 1343 | return; 1344 | } 1345 | // Remove colorization 1346 | $('span.editor-token').css({'background-color': '', 'color': ''}).find("*").css('color', ''); 1347 | $("#wikiwhoAuthorListHeader").text('Conflict View'); 1348 | $("#ageLimitBox").hide(); 1349 | $("#wikiwhoAuthorList").hide(); 1350 | // $(".editor-token").unbind('mouseenter mouseleave'); 1351 | // $(".editor-token").off('mouseenter mouseleave'); 1352 | // Color all tokens 1353 | var conflict_opacity_value = 0; 1354 | for (var i = 0; i < Wikiwho.tokens.length; i++) { 1355 | var conflict_score = Wikiwho.tokens[i][0]; 1356 | if (conflict_score !== 0) { 1357 | conflict_opacity_value = conflict_score/Wikiwho.biggest_conflict_score; 1358 | $('span#token-'+i).css({ 1359 | 'background-color': 'rgba(255,0,0,'+conflict_opacity_value+')', 1360 | 'color': (conflict_opacity_value >= 0.5) ? 'white' : 'black' 1361 | }).find("*").css("color", (conflict_opacity_value >= 0.5) ? 'white' : 'black'); 1362 | } 1363 | } 1364 | // Mark conflict view as open 1365 | Wikiwho.provenanceViewOpen = false; 1366 | Wikiwho.conflictViewOpen = true; 1367 | Wikiwho.ageViewOpen = false; 1368 | $('#provenanceviewbutton').removeClass('provenanceviewbuttonopen'); 1369 | $('#conflictviewbutton').addClass("conflictviewopen"); 1370 | $('#ageviewbutton').removeClass('ageviewbuttonopen'); 1371 | }, 1372 | 1373 | closeConflictView: function() { 1374 | // Remove colorization 1375 | $('span.editor-token').css({'background-color': '', 'color': ''}).find("*").css('color', ''); 1376 | $("#wikiwhoAuthorListHeader").text('Editor List'); 1377 | $("#wikiwhoAuthorList").show(); 1378 | // $(".editor-token").on('mouseenter mouseleave'); 1379 | // Recolor tokens 1380 | Object.keys(Wikiwho.coloredAuthors).forEach(function(authorid) { 1381 | var color = Wikiwho.coloredAuthors[authorid]; 1382 | var contrastColor = Wikiwho.getContrastingColor(color); 1383 | $("span.token-editor-"+authorid).css({"background-color": color, 1384 | "color": contrastColor[0]}).find("*").css("color", contrastColor[1]); 1385 | $('.hvauthorid-'+authorid).css({ 1386 | 'background-color': color, 1387 | 'color': contrastColor[0] 1388 | }); 1389 | }); 1390 | // Mark conflict view as closed 1391 | Wikiwho.provenanceViewOpen = true; 1392 | Wikiwho.conflictViewOpen = false; 1393 | $('#provenanceviewbutton').addClass('provenanceviewbuttonopen'); 1394 | $('#conflictviewbutton').removeClass("conflictviewopen"); 1395 | }, 1396 | 1397 | openAgeView: function() { 1398 | // Remove colorization 1399 | $('span.editor-token').css({'background-color': '', 'color': ''}).find("*").css('color', ''); 1400 | $("#wikiwhoAuthorListHeader").text('Age View'); 1401 | $("#wikiwhoAuthorList").hide(); 1402 | $("#ageLimitBox").show(); 1403 | // Color all tokens according to age 1404 | var shade_count = Math.ceil((Wikiwho.ageLimitTo-Wikiwho.ageLimitFrom)/Wikiwho.groupSize); 1405 | var age_days = 0; 1406 | var age_opacity_value = 0; 1407 | for (var i = 0; i < Wikiwho.tokens.length; i++) { 1408 | age_days = Wikiwho.tokens[i][6] / (60 * 60 * 24); 1409 | if (Wikiwho.ageLimitFrom <= age_days && age_days <= Wikiwho.ageLimitTo) { 1410 | age_opacity_value = (1/shade_count) * (shade_count + 1 - Math.ceil((age_days-Wikiwho.ageLimitFrom)/Wikiwho.groupSize)); 1411 | $('span#token-'+i).css({'background-color': 'rgba(255,255,0,'+age_opacity_value+')'}); 1412 | } 1413 | } 1414 | // Mark age view as open 1415 | Wikiwho.provenanceViewOpen = false; 1416 | Wikiwho.conflictViewOpen = false; 1417 | Wikiwho.ageViewOpen = true; 1418 | $('#provenanceviewbutton').removeClass('provenanceviewbuttonopen'); 1419 | $('#conflictviewbutton').removeClass("conflictviewopen"); 1420 | $('#ageviewbutton').addClass('ageviewbuttonopen'); 1421 | // } 1422 | // else { 1423 | // alert('No token younger than ' + Wikiwho.ageLimit + ' days.'); 1424 | // } 1425 | }, 1426 | 1427 | closeAgeView: function() { 1428 | // Remove colorization 1429 | $('span.editor-token').css({'background-color': '', 'color': ''}).find("*").css('color', ''); 1430 | $("#ageLimitBox").hide(); 1431 | $("#wikiwhoAuthorListHeader").text('Editor List'); 1432 | $("#wikiwhoAuthorList").show(); 1433 | // Recolor tokens 1434 | Object.keys(Wikiwho.coloredAuthors).forEach(function(authorid) { 1435 | var color = Wikiwho.coloredAuthors[authorid]; 1436 | var contrastColor = Wikiwho.getContrastingColor(color); 1437 | $("span.token-editor-"+authorid).css({"background-color": color, 1438 | "color": contrastColor[0]}).find("*").css("color", contrastColor[1]); 1439 | $('.hvauthorid-'+authorid).css({ 1440 | 'background-color': color, 1441 | 'color': contrastColor[0] 1442 | }); 1443 | }); 1444 | // Mark age view as closed 1445 | Wikiwho.provenanceViewOpen = true; 1446 | Wikiwho.ageViewOpen = false; 1447 | $('#provenanceviewbutton').addClass('provenanceviewbuttonopen'); 1448 | $('#ageviewbutton').removeClass("ageviewbuttonopen"); 1449 | }, 1450 | 1451 | // Check whether sth should be done and what (on this specific page) 1452 | pageCheck: function() { 1453 | return $("li#ca-nstab-main").hasClass("selected") && $("li#ca-view").hasClass("selected") 1454 | && !Wikiwho.contentAlreadyReplaced && !$('table.diff').length; 1455 | }, 1456 | 1457 | addStyle: function() { 1458 | GM_addStyle("\ 1459 | #wikiwhorightbar .editor-score {\ 1460 | float: right;\ 1461 | }\ 1462 | #wikiwhorightbar {\ 1463 | border-bottom: none;\ 1464 | position: fixed;\ 1465 | width: calc(15em + 2px);\ 1466 | bottom: 0px;\ 1467 | padding: 0px;\ 1468 | overflow-y: scroll;\ 1469 | }\ 1470 | #wikiwhorightbar > div {\ 1471 | padding: 10px;\ 1472 | margin: 0px;\ 1473 | }\ 1474 | #wikiwhorightbar > div > h2 {\ 1475 | margin-top: 0px;\ 1476 | }\ 1477 | @media screen and (min-width: 982px) {\ 1478 | #wikiwhorightbar {\ 1479 | width: calc(15.5em + 2px);\ 1480 | }\ 1481 | }\ 1482 | ul#wikiwhoAuthorList {\ 1483 | margin: 0px;\ 1484 | }\ 1485 | ul#wikiwhoAuthorList li {\ 1486 | padding: 1px;\ 1487 | padding-right: 3px;\ 1488 | padding-left: 3px;\ 1489 | list-style: none;\ 1490 | }\ 1491 | \ 1492 | ul#wikiwhoAuthorList li:hover, ul#wikiwhoAuthorList li.selected {\ 1493 | border: 1px solid blue;\ 1494 | /*border: 1px solid #aaa;*/\ 1495 | padding: 0px;\ 1496 | padding-right: 2px;\ 1497 | padding-left: 2px;\ 1498 | background-color: #f5fffa;\ 1499 | }\ 1500 | .editor-token.selected, .hvrevauthor.selected {\ 1501 | outline: 1px solid blue;\ 1502 | }\ 1503 | .hvselected, .editor-token-image.hvselected img {\ 1504 | outline: 1px solid red;\ 1505 | }\ 1506 | .editor-token-image.hvselected {\ 1507 | outline: none;\ 1508 | }\ 1509 | .editor-token-image.selected {\ 1510 | outline: none;\ 1511 | }\ 1512 | .editor-token-image.selected img {\ 1513 | outline: 1px solid blue;\ 1514 | }\ 1515 | #wikiwhoseqhistbox {\ 1516 | background-color: rgb(255, 255, 255);\ 1517 | position: fixed;\ 1518 | bottom: 0px;\ 1519 | right: calc(15em + 3px);\ 1520 | left: calc(10em + 1px);\ 1521 | border-top-color: rgb(167, 215, 249);\ 1522 | border-top-style: solid;\ 1523 | border-top-width: 1px;\ 1524 | padding: 1.25em 1.5em 1.5em 1.5em;\ 1525 | white-space: nowrap;\ 1526 | box-sizing: border-box;\ 1527 | }\ 1528 | @media screen and (min-width: 982px) {\ 1529 | #wikiwhoseqhistbox {\ 1530 | right: calc(15.5em + 3px);\ 1531 | left: calc(11em + 1px);\ 1532 | }\ 1533 | }\ 1534 | #wikiwhoseqhistbox .hvcloseicon {\ 1535 | position: absolute;\ 1536 | width: 2em;\ 1537 | top: 0.25em;\ 1538 | left: 0.25em;\ 1539 | cursor: pointer;\ 1540 | }\ 1541 | #wikiwhoseqhistbox.indicator .hvcloseicon {\ 1542 | display: none;\ 1543 | }\ 1544 | #wikiwhoseqhistbox.indicator {\ 1545 | height: 1em;\ 1546 | padding: 0.5em;\ 1547 | top: auto;\ 1548 | box-sizing: content-box;\ 1549 | }\ 1550 | #wikiwhoseqhistboxopenindicator {\ 1551 | text-align: center;\ 1552 | display: none;\ 1553 | }\ 1554 | #wikiwhoseqhistboxonerevindicator {\ 1555 | text-align: center;\ 1556 | display: none;\ 1557 | }\ 1558 | #wikiwhoseqhistbox.indicator:not(.indicatortoolong):not(.indicatoronerev) #wikiwhoseqhistboxopenindicator {\ 1559 | display: block;\ 1560 | }\ 1561 | #wikiwhoseqhistboxtoolongindicator {\ 1562 | text-align: center;\ 1563 | display: none;\ 1564 | }\ 1565 | #wikiwhoseqhistbox.indicatortoolong #wikiwhoseqhistboxtoolongindicator {\ 1566 | display: block;\ 1567 | }\ 1568 | #wikiwhoseqhistbox.indicatoronerev #wikiwhoseqhistboxonerevindicator {\ 1569 | display: block;\ 1570 | }\ 1571 | #wikiwhoseqhistleftbox, #wikiwhoseqhistmiddlebox, #wikiwhoseqhistrightbox {\ 1572 | display: inline-block;\ 1573 | vertical-align: top;\ 1574 | height: 100%;\ 1575 | overflow: hidden;\ 1576 | }\ 1577 | #wikiwhoseqhistmiddlebox {\ 1578 | width: calc(100% - 17em);\ 1579 | }\ 1580 | #wikiwhoseqhistbox.indicator #wikiwhoseqhistview {\ 1581 | display: none;\ 1582 | }\ 1583 | #wikiwhoseqhistview {\ 1584 | position: relative;\ 1585 | }\ 1586 | #wikiwhoseqhistview .hvtokencol {\ 1587 | display: inline-block;\ 1588 | }\ 1589 | #wikiwhoseqhistview .hvtokenhead {\ 1590 | height: 2em;\ 1591 | line-height: 2em;\ 1592 | margin-left: 0.1em;\ 1593 | margin-right: 0.1em;\ 1594 | display: inline-block;\ 1595 | vertical-align: bottom;\ 1596 | }\ 1597 | #wikiwhoseqhistmiddlebox .hvtokenheaders {\ 1598 | position: relative;\ 1599 | overflow: hidden;\ 1600 | right: 0px;\ 1601 | left: 0px;\ 1602 | }\ 1603 | #wikiwhoseqhistmiddlebox .hvtokenbodies {\ 1604 | overflow: auto;\ 1605 | width: 100%;\ 1606 | height: calc(100% - 2em);\ 1607 | }\ 1608 | #wikiwhoseqhistmiddlebox .hvtokencolpiece {\ 1609 | height: calc(4.5em - 1px);\ 1610 | width: 100%;\ 1611 | border-top: dotted 1px blue;\ 1612 | border-left: 1px solid white;\ 1613 | border-right: 1px solid white;\ 1614 | }\ 1615 | #wikiwhoseqhistmiddlebox .hvtokencolpiece:last-child {\ 1616 | border-bottom: dotted 1px blue;\ 1617 | }\ 1618 | #wikiwhoseqhistmiddlebox .hvtokencolpiece.hvtokeninarticle {\ 1619 | background-color: rgb(167, 215, 249);\ 1620 | }\ 1621 | #wikiwhoseqhistleftbox {\ 1622 | margin-top: 1.25em;\ 1623 | height: calc(100% - 1.25em);\ 1624 | position: relative;\ 1625 | }\ 1626 | #wikiwhoseqhistleftbox > div {\ 1627 | height: 4.5em;\ 1628 | line-height: 1.5em;\ 1629 | text-align: right;\ 1630 | }\ 1631 | #wikiwhoseqhistleftbox > div:last-of-type {\ 1632 | height: 1.5em;\ 1633 | margin-bottom: 20px;\ 1634 | }\ 1635 | #wikiwhoseqhistleftbox > div > .hvdatetimediff {\ 1636 | height: 3em;\ 1637 | line-height: 3em;\ 1638 | vertical-align: top;\ 1639 | }\ 1640 | #wikiwhoseqhistleftbox > div .hvupdownarrow {\ 1641 | position: absolute;\ 1642 | left: 2.5em;\ 1643 | margin-top: -0.1em;\ 1644 | font-size: 3em;\ 1645 | }\ 1646 | #wikiwhoseqhistleftbox > div .hvupdownarrow a, #wikiwhoseqhistleftbox > div .hvupdownarrow a:hover, #wikiwhoseqhistleftbox > div .hvupdownarrow a:visited, #wikiwhoseqhistleftbox > div .hvupdownarrow a:link, #wikiwhoseqhistleftbox > div .hvupdownarrow a:active {\ 1647 | color: black;\ 1648 | text-decoration: none;\ 1649 | }\ 1650 | #wikiwhoseqhistleftbox > div .hvupdownarrow a:hover {\ 1651 | color: blue;\ 1652 | }\ 1653 | #wikiwhoseqhistleftbox > div span.hvlefttimediff {\ 1654 | position: absolute;\ 1655 | left: 0.5em;\ 1656 | }\ 1657 | #wikiwhoseqhistleftbox > div span.hvrighttimediff {\ 1658 | position: absolute;\ 1659 | left: 10em;\ 1660 | }\ 1661 | #wikiwhoseqhistleftbox .hvrevauthor {\ 1662 | text-overflow: ellipsis;\ 1663 | overflow: hidden;\ 1664 | max-width: 8em;\ 1665 | }\ 1666 | #wikiwhoseqhistleftbox .hvspacer, #wikiwhoseqhistleftbox .hvspacerauth {\ 1667 | border-bottom: 1px dotted blue;\ 1668 | min-width: 2em;\ 1669 | display: inline-block;\ 1670 | vertical-align: top;\ 1671 | height: 0.75em;\ 1672 | }\ 1673 | #wikiwhoseqhistleftbox .hvspacerauth {\ 1674 | min-width: 0;\ 1675 | white-space: pre;\ 1676 | }\ 1677 | #wikiwhoseqhistleftbox .hvrevauthor, #wikiwhoseqhistleftbox .hvrevdate, #wikiwhoseqhistleftbox .hvrevdifflinks {\ 1678 | display: inline-block;\ 1679 | vertical-align: top;\ 1680 | }\ 1681 | .hvtokenheadspacer {\ 1682 | width: 100px;\ 1683 | display: inline-block;\ 1684 | }\ 1685 | img.hvdifficon {\ 1686 | height: 1.5em;\ 1687 | }\ 1688 | .hvtokencol.hvtokendummycol {\ 1689 | background: rgb(167, 215, 249);\ 1690 | background-image: repeating-linear-gradient(45deg, transparent, transparent 1em, rgba(255,255,255,.5) 1em, rgba(255,255,255,.5) 2em);\ 1691 | }\ 1692 | #provenanceviewbutton {\ 1693 | background-color: white;\ 1694 | height: 24px;\ 1695 | }\ 1696 | #provenanceviewbutton.provenanceviewbuttonopen {\ 1697 | background-color: #00ff00;\ 1698 | }\ 1699 | #conflictviewbutton {\ 1700 | background-color: white;\ 1701 | height: 24px;\ 1702 | }\ 1703 | #conflictviewbutton.conflictviewopen {\ 1704 | background-color: #00ff00;\ 1705 | }\ 1706 | #ageviewbutton {\ 1707 | background-color: white;\ 1708 | height: 24px;\ 1709 | }\ 1710 | #ageviewbutton.ageviewbuttonopen {\ 1711 | background-color: #00ff00;\ 1712 | }\ 1713 | #ageLimitFrom{\ 1714 | float: right;\ 1715 | }\ 1716 | #ageLimitTo{\ 1717 | float: right;\ 1718 | }\ 1719 | #groupSize{\ 1720 | float: right;\ 1721 | }\ 1722 | img.wwhouserinfoicon {\ 1723 | height: 1.5em;\ 1724 | cursor: pointer;\ 1725 | }\ 1726 | img.wwhouserinfoiconhidden {\ 1727 | visibility: hidden;\ 1728 | cursor: default;\ 1729 | }\ 1730 | #wikiwhoAuthorList li span:last-child {\ 1731 | text-overflow: ellipsis;\ 1732 | white-space: nowrap;\ 1733 | overflow: hidden;\ 1734 | width: calc(100% - 4.5em);\ 1735 | display: inline-block;\ 1736 | margin-bottom: -0.4em;\ 1737 | }\ 1738 | "); 1739 | }, 1740 | 1741 | // Initialize the Wikiwho Userscript 1742 | initialize: function() { 1743 | if(!Wikiwho.initialized && Wikiwho.pageCheck()) { 1744 | // We're on a web page where we should do something 1745 | Wikiwho.initialized = true; 1746 | Wikiwho.addStyle(); 1747 | Wikiwho.createHTMLElements(); 1748 | Wikiwho.getWikiwhoData(); 1749 | } 1750 | } 1751 | }; 1752 | 1753 | // Do not run in frames 1754 | if (window.top !== window.self) { 1755 | // Do nothing 1756 | }else{ 1757 | // Initialize the script as soon as the content text / page is loaded 1758 | function waitForMwContent() { 1759 | if($("#mw-content-text").length > 0) { 1760 | Wikiwho.initialize(); 1761 | }else{ 1762 | setTimeout(waitForMwContent, 100); 1763 | } 1764 | } 1765 | 1766 | waitForMwContent(); 1767 | } 1768 | --------------------------------------------------------------------------------