r53911 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r53910‎ | r53911 | r53912 >
Date:23:24, 28 July 2009
Author:tparscal
Status:deferred
Tags:
Comment:
Totally changed tactics, and am now dynamically building the outline on the client side by parsing the wikitext. Still needs work, especially in performance and the UI, but it's working quite nicely!
Modified paths:
  • /trunk/extensions/UsabilityInitiative/NavigableTOC/ISSUES (modified) (history)
  • /trunk/extensions/UsabilityInitiative/NavigableTOC/NavigableTOC.css (modified) (history)
  • /trunk/extensions/UsabilityInitiative/NavigableTOC/NavigableTOC.hooks.php (modified) (history)
  • /trunk/extensions/UsabilityInitiative/NavigableTOC/NavigableTOC.js (modified) (history)

Diff [purge]

Index: trunk/extensions/UsabilityInitiative/NavigableTOC/NavigableTOC.css
@@ -1,5 +1,27 @@
22 /* CSS for NavigableTOC extension */
33
4 -.currentSection {
5 - color: red !important;
 4+div#navigableTOC {
 5+ position: absolute;
 6+ right: 0;
 7+ border: solid 1px silver;
 8+ background-color: #f3f3f3;
 9+ width: 23%;
 10+ padding: 0.5em;
611 }
 12+div#navigableTOC ul {
 13+ margin: 0;
 14+ padding: 0;
 15+ list-style: none;
 16+}
 17+div#navigableTOC ul ul {
 18+ margin-left: 1em;
 19+}
 20+div#navigableTOC ul li a.currentSelection {
 21+ font-weight: bold;
 22+}
 23+div#edittoolbar {
 24+ width: 75%;
 25+}
 26+form#editform textarea#wpTextbox1 {
 27+ width: 75%;
 28+}
\ No newline at end of file
Index: trunk/extensions/UsabilityInitiative/NavigableTOC/NavigableTOC.hooks.php
@@ -26,98 +26,7 @@
2727 UsabilityInitiativeHooks::addStyle(
2828 'NavigableTOC/NavigableTOC.css', $wgNavigableTOCStyleVersion
2929 );
30 -
31 - // Try the parser cache first
32 - $pcache = ParserCache::singleton();
33 - $popts = ParserOptions::newFromUser( $wgUser );
34 - $popts->setTidy( true );
35 - $popts->enableLimitReport();
36 - $articleObj = new Article( $ep->mTitle );
37 - $p_result = $p_result2 = false;
38 - if ( $ep->preview ) {
39 - $p_result = $ep->mParserOutput;
40 - if ( $ep->section != '' ) {
41 - // Store this result and make sure the
42 - // ParserOutput for the entire page is
43 - // grabbed as well
44 - $p_result2 = $p_result;
45 - $p_result = false;
46 - }
47 - }
48 - else if ( $wgEnableParserCache ) {
49 - $p_result = $pcache->get( $articleObj, $popts );
50 - // The ParserOutput in cache could be too old to have
51 - // byte offsets. In that case, reparse
52 - if ( $p_result ) {
53 - $sections = $p_result->getSections();
54 - if ( isset( $sections[0] ) && !isset( $sections[0]['byteoffset'] ) ) {
55 - $p_result = $wgParser->parse( $articleObj->getContent(),
56 - $ep->mTitle, $popts );
57 - if ( $p_result )
58 - $pcache->save( $p_result, $articleObj, $popts );
59 - }
60 - }
61 - }
62 - if ( !$p_result ) {
63 - $popts->setIsPreview( $ep->preview || $ep->section != '' );
64 - $p_result = $wgParser->parse( $articleObj->getContent(),
65 - $ep->mTitle, $popts );
66 - if ( $wgEnableParserCache )
67 - $pcache->save( $p_result, $articleObj,
68 - $popts );
69 - }
70 -
71 - if( $p_result2 ) {
72 - // Merge the section trees of the original article and
73 - // the edited text; this saves us from parsing the
74 - // entire page with the edited section replaced
75 - $sectionTree = Parser::mergeSectionTrees(
76 - $p_result->getSections(),
77 - $p_result2->getSections(),
78 - $ep->section, $ep->mTitle, strlen( $ep->textbox1 ) );
79 - $toc = $wgUser->getSkin()->generateTOC( $sectionTree );
80 - } else {
81 - $sectionTree = $p_result->getSections();
82 - $toc = $p_result->getTOCHTML();
83 - if ( !$toc )
84 - $toc = $wgUser->getSkin()->generateTOC( $sectionTree );
85 - }
86 -
87 - $js = "\$.section = '" . Xml::escapeJsString( $ep->section ) . "';";
88 - $js .= "\$.sectionOffsets = [";
89 - $targetLevel = false;
90 - $targetSection = false;
91 - foreach ( $sectionTree as $section )
92 - if ( !is_null( $section['byteoffset'] ) ) {
93 - if ( $ep->section != '' ) {
94 - // Only get offsets for the section
95 - // being edited and its descendants.
96 - // In preview mode, sibling sections
97 - // may have been added, so use the
98 - // number of sections in $p_result2
99 - if ( $section['index'] < $ep->section )
100 - continue;
101 - else if ( $section['index'] == $ep->section ) {
102 - if ( $ep->preview )
103 - $targetSection = $ep->section +
104 - count( $p_result2->getSections() );
105 - else
106 - $targetLevel = $section['level'];
107 - }
108 - else if ( ( $ep->preview && $section['index'] >= $targetSection ) ||
109 - ( !$ep->preview && $section['level'] <= $targetLevel ) )
110 - break;
111 - }
112 - $js .= intval( $section['byteoffset'] ) . ',';
113 - }
114 - $js .= '];';
115 - $jsTag = Xml::element( 'script', array(), $js );
116 -
117 - // Terrible hack to prevent two TOCs with the same ID
118 - // from being displayed
119 - $toc = str_replace( '<table id="toc"',
120 - '<table id="navigableTOC"', $toc );
121 - $ep->editFormTextTop .= $toc . $jsTag;
 30+ $ep->editFormTextTop .= '<div id="navigableTOC"></div>';
12231 return true;
12332 }
12433 }
Index: trunk/extensions/UsabilityInitiative/NavigableTOC/NavigableTOC.js
@@ -3,75 +3,171 @@
44 /*
55 * This function should be called on the text area to map out the section
66 * character positions by scanning for headings, and the resulting data will
7 - * be stored as $(this).data( 'sections', { ... } )
 7+ * be stored as $(this).data( 'outline', { ... } )
88 */
9 -jQuery.fn.mapSections = function() {
 9+jQuery.fn.parseOutline = function() {
1010 return this.each( function() {
11 - // WRITE CODE HERE :)
 11+ // Extract headings from wikitext
 12+ var wikitext = '\r\n' + $(this).val() + '\r\n';
 13+ var headings = wikitext.match( /[\r\n][=]+[^\r\n]*[=]+[\r\n]/g );
 14+ var outline = [];
 15+ var offset = 0;
 16+ for ( var h = 0; h < headings.length; h++ ) {
 17+ text = headings[h];
 18+ // Get position of first occurence
 19+ var position = wikitext.indexOf( text, offset );
 20+ // Update offset to avoid stumbling on duplicate headings
 21+ if ( position > offset ) {
 22+ offset = position;
 23+ } else if ( position == -1 ) {
 24+ // Not sure this is possible, or what should happen
 25+ continue;
 26+ }
 27+ // Trim off whitespace
 28+ text = jQuery.trim( text );
 29+ // Detect the starting and ending heading levels
 30+ var startLevel = 0;
 31+ for ( var c = 0; c < text.length; c++ ) {
 32+ if ( text.charAt( c ) == '=' ) {
 33+ startLevel++;
 34+ } else {
 35+ break;
 36+ }
 37+ }
 38+ var endLevel = 0;
 39+ for ( var c = text.length - 1; c >= 0; c-- ) {
 40+ if ( text.charAt( c ) == '=' ) {
 41+ endLevel++;
 42+ } else {
 43+ break;
 44+ }
 45+ }
 46+ // Use the lowest common denominator as the actual level
 47+ var level = Math.min( startLevel, endLevel );
 48+ text = jQuery.trim( text.substr( level, text.length - ( level * 2 ) ) );
 49+ // Add the heading data to the outline
 50+ outline[h] = {
 51+ 'text': text,
 52+ 'position': position,
 53+ 'level': level,
 54+ 'index': h
 55+ };
 56+ /*
 57+ console.log(
 58+ 'heading:%s @ %i # %i-%i',
 59+ text,
 60+ position,
 61+ startLevel,
 62+ endLevel
 63+ );
 64+ */
 65+ }
 66+ // Cache outline
 67+ $(this).data( 'outline', outline )
1268 } );
1369 };
1470 /*
15 - * This function should be called on the text area with a selected UL element
16 - * to perform the list update on, where it will match the current cursor
17 - * position to an item on the outline and classify that li as 'current'
 71+ * Generate structured UL from outline
1872 */
19 -jQuery.fn.updateSectionsList = function( list ) {
20 - return this.each( function( list ) {
21 - // WRITE CODE HERE :)
 73+jQuery.fn.buildOutline = function( target ) {
 74+ return this.each( function() {
 75+ if ( target.size() ) {
 76+ var outline = $(this).data( 'outline' );
 77+ // Normalize levels, adding an nLevel parameter to each node
 78+ var level = 1;
 79+ for ( var i = 0; i < outline.length; i++ ) {
 80+ if ( i > 0 ) {
 81+ if ( outline[i].level > outline[i - 1].level ) {
 82+ level++;
 83+ } else if ( outline[i].level < outline[i - 1].level ) {
 84+ level -= Math.max(
 85+ 1, outline[i - 1].level - outline[i].level
 86+ );
 87+ }
 88+ }
 89+ outline[i].nLevel = level;
 90+ /*
 91+ console.log(
 92+ '%s %s',
 93+ ( new Array( level + 1 ).join( ':' ) ),
 94+ outline[i].text
 95+ );
 96+ */
 97+ }
 98+
 99+ function buildStructure( outline, structure, offset, level ) {
 100+ if ( offset == undefined ) offset = 0;
 101+ if ( level == undefined ) level = 1;
 102+ for ( var i = offset; i < outline.length; i++ ) {
 103+ if ( outline[i].nLevel == level ) {
 104+ buildStructure( outline, outline[i], i + 1, level + 1 );
 105+ if ( structure.sections == undefined ) {
 106+ structure.sections = [ outline[i] ];
 107+ } else {
 108+ structure.sections[structure.sections.length] = outline[i];
 109+ }
 110+ } else if ( outline[i].nLevel < level ) {
 111+ break;
 112+ }
 113+ }
 114+ }
 115+ function buildList( textarea, structure ) {
 116+ var list = $( '<ul><ul>' );
 117+ for ( i in structure ) {
 118+ var item = $( '<li></li>' )
 119+ .append(
 120+ $( '<a></a>' )
 121+ .attr( 'href', '#' )
 122+ .addClass( 'section-' + structure[i].index )
 123+ .data( 'textbox', textarea )
 124+ .data( 'position', structure[i].position )
 125+ .click( function( event ) {
 126+ $(this).data( 'textbox' ).scrollToPosition(
 127+ $(this).data( 'position' ) - 1
 128+ );
 129+ event.preventDefault();
 130+ } )
 131+ .text( structure[i].text )
 132+ );
 133+ if ( structure[i].sections !== undefined ) {
 134+ item.append( buildList( textarea, structure[i].sections ) );
 135+ }
 136+ list.append( item );
 137+ }
 138+ return list;
 139+ }
 140+ var structure = {};
 141+ buildStructure( outline, structure );
 142+ target.html( buildList( $(this), structure.sections ) );
 143+ }
22144 } );
23145 };
24 -
25 -$( document ).ready( function() {
26 - if ( $.section == '' ) {
27 - // Full page edit
28 - // Tell the section links what their offsets are
29 - for ( i = 0; i < $.sectionOffsets.length; i++ )
30 - $( '.tocsection-' + ( i + 1 ) ).children( 'a' )
31 - .data( 'offset', $.sectionOffsets[i] );
32 - } else if ( $.section != 'new' && $.section != 0 ) {
33 - // Existing section edit
34 - // Set adjusted offsets on the usable links
35 - $.section = parseInt( $.section );
36 - for ( i = 0; i < $.sectionOffsets.length; i++ )
37 - $( '.tocsection-' + ( i + $.section ) ).children( 'a' )
38 - .data( 'offset', $.sectionOffsets[i] -
39 - $.sectionOffsets[0] );
40 - }
41 - // Unlink all section links that didn't get an offset
42 - $( '.toc:last * li' ).each( function() {
43 - link = $(this).children( 'a:visible' );
44 - if ( typeof link.data( 'offset') == 'undefined' ) {
45 - link.hide();
46 - $(this).prepend( link.html() );
47 - }
48 - });
49 -
50 - $( '.toc:last * li a' ).click( function(e) {
51 - if( typeof $(this).data( 'offset' ) != 'undefined' )
52 - $( '#wpTextbox1' ).scrollToPosition( $(this).data( 'offset' ) );
53 - e.preventDefault();
54 - });
55 -
56 - function styleCurrentSection() {
57 - // FIXME: Try to dynamically adjust section offsets when user
58 - // enters/removes stuff
59 - // Find the section we're in
60 - bytePos = $( '#wpTextbox1' ).bytePos();
61 - i = 0;
62 - while ( i < $.sectionOffsets.length &&
63 - $.sectionOffsets[i] <= bytePos )
 146+/*
 147+ * Highlight the section the cursor is currently within
 148+ */
 149+jQuery.fn.updateOutline = function( target ) {
 150+ return this.each( function() {
 151+ var outline = $(this).data( 'outline' );
 152+ var position = $(this).bytePos();
 153+ var i = 0;
 154+ while ( i < outline.length && outline[i].position - 1 <= position ) {
64155 i++;
65 - sectionLink = $( '.tocsection-' + i ).children( 'a' );
66 - if ( !sectionLink.hasClass( 'currentSection' ) ) {
67 - $( '.currentSection' ).removeClass( 'currentSection' );
68 - sectionLink.addClass( 'currentSection' );
69156 }
70 - }
71 -
72 - $( '#wpTextbox1' ).bind( 'keydown mousedown scrollToPosition', function() {
73 - // Run styleCurrentSelection() after event processing is done
74 - // If we run it directly, we'll get an out-of-date byte position
75 - // This is ugly as hell
76 - setTimeout(styleCurrentSection, 0);
77 - });
 157+ i = Math.max( 0, i - 1 );
 158+ target.find( 'a' ).removeClass( 'currentSelection' );
 159+ target.find( 'a.section-' + i ).addClass( 'currentSelection' );
 160+ } );
 161+};
 162+$( document ).ready( function() {
 163+ $( '#wpTextbox1' ).parseOutline();
 164+ $( '#wpTextbox1' )
 165+ .buildOutline( $( '#navigableTOC' ) )
 166+ .updateOutline( $( '#navigableTOC' ) )
 167+ .bind( 'keyup', { 'list': $( '#navigableTOC' ) }, function( event ) {
 168+ $(this).parseOutline();
 169+ $(this).buildOutline( event.data.list );
 170+ } )
 171+ .bind( 'keyup mouseup scrollToPosition', function() {
 172+ $(this).updateOutline( $( '#navigableTOC' ) );
 173+ } );
78174 });
Index: trunk/extensions/UsabilityInitiative/NavigableTOC/ISSUES
@@ -1,8 +1,4 @@
2 -* Script assumes lines only break on spaces, but in practice lines can also be
3 - broken mid-word on - / & etc. This means the presence of such breaks (e.g.
4 - in URLs) causes the script to scroll down a few lines too few or too many.
5 - Especially the latter is very confusing.
6 -* Script assumes the letters in the textarea are 8x16 (Windows), 8x13 (Mac) or
7 - 7x15 (Linux), but these values should really be determined dynamically.
8 -** Both of these can be avoided by determining the caret position properly,
9 - I'll ask Michael Dale about this.
 2+* The UI is still a horrible hack
 3+* parseOutline doesn't play well with headings that don't have lines above and
 4+below
 5+* The buildList logic is flawed
\ No newline at end of file

Status & tagging log