r105974 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r105973‎ | r105974 | r105975 >
Date:03:03, 13 December 2011
Author:neilk
Status:ok (Comments)
Tags:
Comment:
moved language library to core mediawiki.jqueryMsg
Modified paths:
  • /trunk/phase3/resources/mediawiki/mediawiki.jqueryMsg.js (added) (history)
  • /trunk/phase3/resources/mediawiki/mediawiki.jqueryMsg.peg (added) (history)
  • /trunk/phase3/tests/jasmine/spec/mediawiki.jqueryMsg.spec.data.js (added) (history)
  • /trunk/phase3/tests/jasmine/spec/mediawiki.jqueryMsg.spec.js (added) (history)
  • /trunk/phase3/tests/jasmine/spec_makers/makeJqueryMsgSpec.php (added) (history)

Diff [purge]

Index: trunk/phase3/tests/jasmine/spec/mediawiki.jqueryMsg.spec.data.js
@@ -0,0 +1,488 @@
 2+// This file stores the results from the PHP parser for certain messages and arguments,
 3+// so we can test the equivalent Javascript libraries.
 4+// Last generated with makeLanguageSpec.php at 2011-01-28T02:04:09+00:00
 5+
 6+mediaWiki.messages.set( {
 7+ "en_undelete_short": "Undelete {{PLURAL:$1|one edit|$1 edits}}",
 8+ "en_category-subcat-count": "{{PLURAL:$2|This category has only the following subcategory.|This category has the following {{PLURAL:$1|subcategory|$1 subcategories}}, out of $2 total.}}",
 9+ "fr_undelete_short": "Restaurer $1 modification{{PLURAL:$1||s}}",
 10+ "fr_category-subcat-count": "Cette cat\u00e9gorie comprend {{PLURAL:$2|la sous-cat\u00e9gorie|$2 sous-cat\u00e9gories, dont {{PLURAL:$1|celle|les $1}}}} ci-dessous.",
 11+ "ar_undelete_short": "\u0627\u0633\u062a\u0631\u062c\u0627\u0639 {{PLURAL:$1|\u062a\u0639\u062f\u064a\u0644 \u0648\u0627\u062d\u062f|\u062a\u0639\u062f\u064a\u0644\u064a\u0646|$1 \u062a\u0639\u062f\u064a\u0644\u0627\u062a|$1 \u062a\u0639\u062f\u064a\u0644|$1 \u062a\u0639\u062f\u064a\u0644\u0627}}",
 12+ "ar_category-subcat-count": "{{PLURAL:$2|\u0644\u0627 \u062a\u0635\u0627\u0646\u064a\u0641 \u0641\u0631\u0639\u064a\u0629 \u0641\u064a \u0647\u0630\u0627 \u0627\u0644\u062a\u0635\u0646\u064a\u0641|\u0647\u0630\u0627 \u0627\u0644\u062a\u0635\u0646\u064a\u0641 \u0641\u064a\u0647 \u0627\u0644\u062a\u0635\u0646\u064a\u0641 \u0627\u0644\u0641\u0631\u0639\u064a \u0627\u0644\u062a\u0627\u0644\u064a \u0641\u0642\u0637.|\u0647\u0630\u0627 \u0627\u0644\u062a\u0635\u0646\u064a\u0641 \u0641\u064a\u0647 {{PLURAL:$1||\u0647\u0630\u0627 \u0627\u0644\u062a\u0635\u0646\u064a\u0641 \u0627\u0644\u0641\u0631\u0639\u064a|\u0647\u0630\u064a\u0646 \u0627\u0644\u062a\u0635\u0646\u064a\u0641\u064a\u0646 \u0627\u0644\u0641\u0631\u0639\u064a\u064a\u0646|\u0647\u0630\u0647 \u0627\u0644$1 \u062a\u0635\u0627\u0646\u064a\u0641 \u0627\u0644\u0641\u0631\u0639\u064a\u0629|\u0647\u0630\u0647 \u0627\u0644$1 \u062a\u0635\u0646\u064a\u0641\u0627 \u0641\u0631\u0639\u064a\u0627|\u0647\u0630\u0647 \u0627\u0644$1 \u062a\u0635\u0646\u064a\u0641 \u0641\u0631\u0639\u064a}}\u060c \u0645\u0646 \u0625\u062c\u0645\u0627\u0644\u064a $2.}}",
 13+ "jp_undelete_short": "Undelete {{PLURAL:$1|one edit|$1 edits}}",
 14+ "jp_category-subcat-count": "{{PLURAL:$2|This category has only the following subcategory.|This category has the following {{PLURAL:$1|subcategory|$1 subcategories}}, out of $2 total.}}",
 15+ "zh_undelete_short": "\u6062\u590d\u88ab\u5220\u9664\u7684$1\u9879\u4fee\u8ba2",
 16+ "zh_category-subcat-count": "{{PLURAL:$2|\u672c\u5206\u7c7b\u53ea\u6709\u4e0b\u5217\u4e00\u4e2a\u5b50\u5206\u7c7b\u3002|\u672c\u5206\u7c7b\u5305\u542b\u4e0b\u5217$1\u4e2a\u5b50\u5206\u7c7b\uff0c\u5171\u6709$2\u4e2a\u5b50\u5206\u7c7b\u3002}}"
 17+} );
 18+var jasmineMsgSpec = [
 19+ {
 20+ "name": "en undelete_short 0",
 21+ "key": "en_undelete_short",
 22+ "args": [
 23+ 0
 24+ ],
 25+ "result": "Undelete 0 edits",
 26+ "lang": "en"
 27+ },
 28+ {
 29+ "name": "en undelete_short 1",
 30+ "key": "en_undelete_short",
 31+ "args": [
 32+ 1
 33+ ],
 34+ "result": "Undelete one edit",
 35+ "lang": "en"
 36+ },
 37+ {
 38+ "name": "en undelete_short 2",
 39+ "key": "en_undelete_short",
 40+ "args": [
 41+ 2
 42+ ],
 43+ "result": "Undelete 2 edits",
 44+ "lang": "en"
 45+ },
 46+ {
 47+ "name": "en undelete_short 5",
 48+ "key": "en_undelete_short",
 49+ "args": [
 50+ 5
 51+ ],
 52+ "result": "Undelete 5 edits",
 53+ "lang": "en"
 54+ },
 55+ {
 56+ "name": "en undelete_short 21",
 57+ "key": "en_undelete_short",
 58+ "args": [
 59+ 21
 60+ ],
 61+ "result": "Undelete 21 edits",
 62+ "lang": "en"
 63+ },
 64+ {
 65+ "name": "en undelete_short 101",
 66+ "key": "en_undelete_short",
 67+ "args": [
 68+ 101
 69+ ],
 70+ "result": "Undelete 101 edits",
 71+ "lang": "en"
 72+ },
 73+ {
 74+ "name": "en category-subcat-count 0,10",
 75+ "key": "en_category-subcat-count",
 76+ "args": [
 77+ 0,
 78+ 10
 79+ ],
 80+ "result": "This category has the following 0 subcategories, out of 10 total.",
 81+ "lang": "en"
 82+ },
 83+ {
 84+ "name": "en category-subcat-count 1,1",
 85+ "key": "en_category-subcat-count",
 86+ "args": [
 87+ 1,
 88+ 1
 89+ ],
 90+ "result": "This category has only the following subcategory.",
 91+ "lang": "en"
 92+ },
 93+ {
 94+ "name": "en category-subcat-count 1,2",
 95+ "key": "en_category-subcat-count",
 96+ "args": [
 97+ 1,
 98+ 2
 99+ ],
 100+ "result": "This category has the following subcategory, out of 2 total.",
 101+ "lang": "en"
 102+ },
 103+ {
 104+ "name": "en category-subcat-count 3,30",
 105+ "key": "en_category-subcat-count",
 106+ "args": [
 107+ 3,
 108+ 30
 109+ ],
 110+ "result": "This category has the following 3 subcategories, out of 30 total.",
 111+ "lang": "en"
 112+ },
 113+ {
 114+ "name": "fr undelete_short 0",
 115+ "key": "fr_undelete_short",
 116+ "args": [
 117+ 0
 118+ ],
 119+ "result": "Restaurer 0 modification",
 120+ "lang": "fr"
 121+ },
 122+ {
 123+ "name": "fr undelete_short 1",
 124+ "key": "fr_undelete_short",
 125+ "args": [
 126+ 1
 127+ ],
 128+ "result": "Restaurer 1 modification",
 129+ "lang": "fr"
 130+ },
 131+ {
 132+ "name": "fr undelete_short 2",
 133+ "key": "fr_undelete_short",
 134+ "args": [
 135+ 2
 136+ ],
 137+ "result": "Restaurer 2 modifications",
 138+ "lang": "fr"
 139+ },
 140+ {
 141+ "name": "fr undelete_short 5",
 142+ "key": "fr_undelete_short",
 143+ "args": [
 144+ 5
 145+ ],
 146+ "result": "Restaurer 5 modifications",
 147+ "lang": "fr"
 148+ },
 149+ {
 150+ "name": "fr undelete_short 21",
 151+ "key": "fr_undelete_short",
 152+ "args": [
 153+ 21
 154+ ],
 155+ "result": "Restaurer 21 modifications",
 156+ "lang": "fr"
 157+ },
 158+ {
 159+ "name": "fr undelete_short 101",
 160+ "key": "fr_undelete_short",
 161+ "args": [
 162+ 101
 163+ ],
 164+ "result": "Restaurer 101 modifications",
 165+ "lang": "fr"
 166+ },
 167+ {
 168+ "name": "fr category-subcat-count 0,10",
 169+ "key": "fr_category-subcat-count",
 170+ "args": [
 171+ 0,
 172+ 10
 173+ ],
 174+ "result": "Cette cat\u00e9gorie comprend 10 sous-cat\u00e9gories, dont celle ci-dessous.",
 175+ "lang": "fr"
 176+ },
 177+ {
 178+ "name": "fr category-subcat-count 1,1",
 179+ "key": "fr_category-subcat-count",
 180+ "args": [
 181+ 1,
 182+ 1
 183+ ],
 184+ "result": "Cette cat\u00e9gorie comprend la sous-cat\u00e9gorie ci-dessous.",
 185+ "lang": "fr"
 186+ },
 187+ {
 188+ "name": "fr category-subcat-count 1,2",
 189+ "key": "fr_category-subcat-count",
 190+ "args": [
 191+ 1,
 192+ 2
 193+ ],
 194+ "result": "Cette cat\u00e9gorie comprend 2 sous-cat\u00e9gories, dont celle ci-dessous.",
 195+ "lang": "fr"
 196+ },
 197+ {
 198+ "name": "fr category-subcat-count 3,30",
 199+ "key": "fr_category-subcat-count",
 200+ "args": [
 201+ 3,
 202+ 30
 203+ ],
 204+ "result": "Cette cat\u00e9gorie comprend 30 sous-cat\u00e9gories, dont les 3 ci-dessous.",
 205+ "lang": "fr"
 206+ },
 207+ {
 208+ "name": "ar undelete_short 0",
 209+ "key": "ar_undelete_short",
 210+ "args": [
 211+ 0
 212+ ],
 213+ "result": "\u0627\u0633\u062a\u0631\u062c\u0627\u0639 \u062a\u0639\u062f\u064a\u0644 \u0648\u0627\u062d\u062f",
 214+ "lang": "ar"
 215+ },
 216+ {
 217+ "name": "ar undelete_short 1",
 218+ "key": "ar_undelete_short",
 219+ "args": [
 220+ 1
 221+ ],
 222+ "result": "\u0627\u0633\u062a\u0631\u062c\u0627\u0639 \u062a\u0639\u062f\u064a\u0644\u064a\u0646",
 223+ "lang": "ar"
 224+ },
 225+ {
 226+ "name": "ar undelete_short 2",
 227+ "key": "ar_undelete_short",
 228+ "args": [
 229+ 2
 230+ ],
 231+ "result": "\u0627\u0633\u062a\u0631\u062c\u0627\u0639 2 \u062a\u0639\u062f\u064a\u0644\u0627\u062a",
 232+ "lang": "ar"
 233+ },
 234+ {
 235+ "name": "ar undelete_short 5",
 236+ "key": "ar_undelete_short",
 237+ "args": [
 238+ 5
 239+ ],
 240+ "result": "\u0627\u0633\u062a\u0631\u062c\u0627\u0639 5 \u062a\u0639\u062f\u064a\u0644",
 241+ "lang": "ar"
 242+ },
 243+ {
 244+ "name": "ar undelete_short 21",
 245+ "key": "ar_undelete_short",
 246+ "args": [
 247+ 21
 248+ ],
 249+ "result": "\u0627\u0633\u062a\u0631\u062c\u0627\u0639 21 \u062a\u0639\u062f\u064a\u0644\u0627",
 250+ "lang": "ar"
 251+ },
 252+ {
 253+ "name": "ar undelete_short 101",
 254+ "key": "ar_undelete_short",
 255+ "args": [
 256+ 101
 257+ ],
 258+ "result": "\u0627\u0633\u062a\u0631\u062c\u0627\u0639 101 \u062a\u0639\u062f\u064a\u0644\u0627",
 259+ "lang": "ar"
 260+ },
 261+ {
 262+ "name": "ar category-subcat-count 0,10",
 263+ "key": "ar_category-subcat-count",
 264+ "args": [
 265+ 0,
 266+ 10
 267+ ],
 268+ "result": "\u0647\u0630\u0627 \u0627\u0644\u062a\u0635\u0646\u064a\u0641 \u0641\u064a\u0647 \u060c \u0645\u0646 \u0625\u062c\u0645\u0627\u0644\u064a 10.",
 269+ "lang": "ar"
 270+ },
 271+ {
 272+ "name": "ar category-subcat-count 1,1",
 273+ "key": "ar_category-subcat-count",
 274+ "args": [
 275+ 1,
 276+ 1
 277+ ],
 278+ "result": "\u0647\u0630\u0627 \u0627\u0644\u062a\u0635\u0646\u064a\u0641 \u0641\u064a\u0647 \u0627\u0644\u062a\u0635\u0646\u064a\u0641 \u0627\u0644\u0641\u0631\u0639\u064a \u0627\u0644\u062a\u0627\u0644\u064a \u0641\u0642\u0637.",
 279+ "lang": "ar"
 280+ },
 281+ {
 282+ "name": "ar category-subcat-count 1,2",
 283+ "key": "ar_category-subcat-count",
 284+ "args": [
 285+ 1,
 286+ 2
 287+ ],
 288+ "result": "\u0647\u0630\u0627 \u0627\u0644\u062a\u0635\u0646\u064a\u0641 \u0641\u064a\u0647 \u0647\u0630\u0627 \u0627\u0644\u062a\u0635\u0646\u064a\u0641 \u0627\u0644\u0641\u0631\u0639\u064a\u060c \u0645\u0646 \u0625\u062c\u0645\u0627\u0644\u064a 2.",
 289+ "lang": "ar"
 290+ },
 291+ {
 292+ "name": "ar category-subcat-count 3,30",
 293+ "key": "ar_category-subcat-count",
 294+ "args": [
 295+ 3,
 296+ 30
 297+ ],
 298+ "result": "\u0647\u0630\u0627 \u0627\u0644\u062a\u0635\u0646\u064a\u0641 \u0641\u064a\u0647 \u0647\u0630\u0647 \u0627\u06443 \u062a\u0635\u0627\u0646\u064a\u0641 \u0627\u0644\u0641\u0631\u0639\u064a\u0629\u060c \u0645\u0646 \u0625\u062c\u0645\u0627\u0644\u064a 30.",
 299+ "lang": "ar"
 300+ },
 301+ {
 302+ "name": "jp undelete_short 0",
 303+ "key": "jp_undelete_short",
 304+ "args": [
 305+ 0
 306+ ],
 307+ "result": "Undelete 0 edits",
 308+ "lang": "jp"
 309+ },
 310+ {
 311+ "name": "jp undelete_short 1",
 312+ "key": "jp_undelete_short",
 313+ "args": [
 314+ 1
 315+ ],
 316+ "result": "Undelete one edit",
 317+ "lang": "jp"
 318+ },
 319+ {
 320+ "name": "jp undelete_short 2",
 321+ "key": "jp_undelete_short",
 322+ "args": [
 323+ 2
 324+ ],
 325+ "result": "Undelete 2 edits",
 326+ "lang": "jp"
 327+ },
 328+ {
 329+ "name": "jp undelete_short 5",
 330+ "key": "jp_undelete_short",
 331+ "args": [
 332+ 5
 333+ ],
 334+ "result": "Undelete 5 edits",
 335+ "lang": "jp"
 336+ },
 337+ {
 338+ "name": "jp undelete_short 21",
 339+ "key": "jp_undelete_short",
 340+ "args": [
 341+ 21
 342+ ],
 343+ "result": "Undelete 21 edits",
 344+ "lang": "jp"
 345+ },
 346+ {
 347+ "name": "jp undelete_short 101",
 348+ "key": "jp_undelete_short",
 349+ "args": [
 350+ 101
 351+ ],
 352+ "result": "Undelete 101 edits",
 353+ "lang": "jp"
 354+ },
 355+ {
 356+ "name": "jp category-subcat-count 0,10",
 357+ "key": "jp_category-subcat-count",
 358+ "args": [
 359+ 0,
 360+ 10
 361+ ],
 362+ "result": "This category has the following 0 subcategories, out of 10 total.",
 363+ "lang": "jp"
 364+ },
 365+ {
 366+ "name": "jp category-subcat-count 1,1",
 367+ "key": "jp_category-subcat-count",
 368+ "args": [
 369+ 1,
 370+ 1
 371+ ],
 372+ "result": "This category has only the following subcategory.",
 373+ "lang": "jp"
 374+ },
 375+ {
 376+ "name": "jp category-subcat-count 1,2",
 377+ "key": "jp_category-subcat-count",
 378+ "args": [
 379+ 1,
 380+ 2
 381+ ],
 382+ "result": "This category has the following subcategory, out of 2 total.",
 383+ "lang": "jp"
 384+ },
 385+ {
 386+ "name": "jp category-subcat-count 3,30",
 387+ "key": "jp_category-subcat-count",
 388+ "args": [
 389+ 3,
 390+ 30
 391+ ],
 392+ "result": "This category has the following 3 subcategories, out of 30 total.",
 393+ "lang": "jp"
 394+ },
 395+ {
 396+ "name": "zh undelete_short 0",
 397+ "key": "zh_undelete_short",
 398+ "args": [
 399+ 0
 400+ ],
 401+ "result": "\u6062\u590d\u88ab\u5220\u9664\u76840\u9879\u4fee\u8ba2",
 402+ "lang": "zh"
 403+ },
 404+ {
 405+ "name": "zh undelete_short 1",
 406+ "key": "zh_undelete_short",
 407+ "args": [
 408+ 1
 409+ ],
 410+ "result": "\u6062\u590d\u88ab\u5220\u9664\u76841\u9879\u4fee\u8ba2",
 411+ "lang": "zh"
 412+ },
 413+ {
 414+ "name": "zh undelete_short 2",
 415+ "key": "zh_undelete_short",
 416+ "args": [
 417+ 2
 418+ ],
 419+ "result": "\u6062\u590d\u88ab\u5220\u9664\u76842\u9879\u4fee\u8ba2",
 420+ "lang": "zh"
 421+ },
 422+ {
 423+ "name": "zh undelete_short 5",
 424+ "key": "zh_undelete_short",
 425+ "args": [
 426+ 5
 427+ ],
 428+ "result": "\u6062\u590d\u88ab\u5220\u9664\u76845\u9879\u4fee\u8ba2",
 429+ "lang": "zh"
 430+ },
 431+ {
 432+ "name": "zh undelete_short 21",
 433+ "key": "zh_undelete_short",
 434+ "args": [
 435+ 21
 436+ ],
 437+ "result": "\u6062\u590d\u88ab\u5220\u9664\u768421\u9879\u4fee\u8ba2",
 438+ "lang": "zh"
 439+ },
 440+ {
 441+ "name": "zh undelete_short 101",
 442+ "key": "zh_undelete_short",
 443+ "args": [
 444+ 101
 445+ ],
 446+ "result": "\u6062\u590d\u88ab\u5220\u9664\u7684101\u9879\u4fee\u8ba2",
 447+ "lang": "zh"
 448+ },
 449+ {
 450+ "name": "zh category-subcat-count 0,10",
 451+ "key": "zh_category-subcat-count",
 452+ "args": [
 453+ 0,
 454+ 10
 455+ ],
 456+ "result": "\u672c\u5206\u7c7b\u5305\u542b\u4e0b\u52170\u4e2a\u5b50\u5206\u7c7b\uff0c\u5171\u670910\u4e2a\u5b50\u5206\u7c7b\u3002",
 457+ "lang": "zh"
 458+ },
 459+ {
 460+ "name": "zh category-subcat-count 1,1",
 461+ "key": "zh_category-subcat-count",
 462+ "args": [
 463+ 1,
 464+ 1
 465+ ],
 466+ "result": "\u672c\u5206\u7c7b\u53ea\u6709\u4e0b\u5217\u4e00\u4e2a\u5b50\u5206\u7c7b\u3002",
 467+ "lang": "zh"
 468+ },
 469+ {
 470+ "name": "zh category-subcat-count 1,2",
 471+ "key": "zh_category-subcat-count",
 472+ "args": [
 473+ 1,
 474+ 2
 475+ ],
 476+ "result": "\u672c\u5206\u7c7b\u5305\u542b\u4e0b\u52171\u4e2a\u5b50\u5206\u7c7b\uff0c\u5171\u67092\u4e2a\u5b50\u5206\u7c7b\u3002",
 477+ "lang": "zh"
 478+ },
 479+ {
 480+ "name": "zh category-subcat-count 3,30",
 481+ "key": "zh_category-subcat-count",
 482+ "args": [
 483+ 3,
 484+ 30
 485+ ],
 486+ "result": "\u672c\u5206\u7c7b\u5305\u542b\u4e0b\u52173\u4e2a\u5b50\u5206\u7c7b\uff0c\u5171\u670930\u4e2a\u5b50\u5206\u7c7b\u3002",
 487+ "lang": "zh"
 488+ }
 489+];
Property changes on: trunk/phase3/tests/jasmine/spec/mediawiki.jqueryMsg.spec.data.js
___________________________________________________________________
Added: svn:eol-style
1490 + native
Index: trunk/phase3/tests/jasmine/spec/mediawiki.jqueryMsg.spec.js
@@ -0,0 +1,350 @@
 2+/* spec for language & message behaviour in MediaWiki */
 3+
 4+mw.messages.set( {
 5+ "en_empty": "",
 6+ "en_simple": "Simple message",
 7+ "en_replace": "Simple $1 replacement",
 8+ "en_replace2": "Simple $1 $2 replacements",
 9+ "en_link": "Simple [http://example.com link to example].",
 10+ "en_link_replace": "Complex [$1 $2] behaviour.",
 11+ "en_simple_magic": "Simple {{ALOHOMORA}} message",
 12+ "en_undelete_short": "Undelete {{PLURAL:$1|one edit|$1 edits}}",
 13+ "en_undelete_empty_param": "Undelete{{PLURAL:$1|| multiple edits}}",
 14+ "en_category-subcat-count": "{{PLURAL:$2|This category has only the following subcategory.|This category has the following {{PLURAL:$1|subcategory|$1 subcategories}}, out of $2 total.}}",
 15+ "en_escape0": "Escape \\to fantasy island",
 16+ "en_escape1": "I had \\$2.50 in my pocket",
 17+ "en_escape2": "I had {{PLURAL:$1|the absolute \\|$1\\| which came out to \\$3.00 in my C:\\\\drive| some stuff}}",
 18+ "en_fail": "This should fail to {{parse",
 19+ "en_fail_magic": "There is no such magic word as {{SIETNAME}}"
 20+} );
 21+
 22+/**
 23+ * Tests
 24+ */
 25+( function( mw, $, undefined ) {
 26+
 27+ describe( "mediaWiki.jqueryMsg", function() {
 28+
 29+ describe( "basic message functionality", function() {
 30+
 31+ it( "should return identity for empty string", function() {
 32+ var parser = new mw.jqueryMsg.parser();
 33+ expect( parser.parse( 'en_empty' ).html() ).toEqual( '' );
 34+ } );
 35+
 36+
 37+ it( "should return identity for simple string", function() {
 38+ var parser = new mw.jqueryMsg.parser();
 39+ expect( parser.parse( 'en_simple' ).html() ).toEqual( 'Simple message' );
 40+ } );
 41+
 42+ } );
 43+
 44+ describe( "escaping", function() {
 45+
 46+ it ( "should handle simple escaping", function() {
 47+ var parser = new mw.jqueryMsg.parser();
 48+ expect( parser.parse( 'en_escape0' ).html() ).toEqual( 'Escape to fantasy island' );
 49+ } );
 50+
 51+ it ( "should escape dollar signs found in ordinary text when backslashed", function() {
 52+ var parser = new mw.jqueryMsg.parser();
 53+ expect( parser.parse( 'en_escape1' ).html() ).toEqual( 'I had $2.50 in my pocket' );
 54+ } );
 55+
 56+ it ( "should handle a complicated escaping case, including escaped pipe chars in template args", function() {
 57+ var parser = new mw.jqueryMsg.parser();
 58+ expect( parser.parse( 'en_escape2', [ 1 ] ).html() ).toEqual( 'I had the absolute |1| which came out to $3.00 in my C:\\drive' );
 59+ } );
 60+
 61+ } );
 62+
 63+ describe( "replacing", function() {
 64+
 65+ it ( "should handle simple replacing", function() {
 66+ var parser = new mw.jqueryMsg.parser();
 67+ expect( parser.parse( 'en_replace', [ 'foo' ] ).html() ).toEqual( 'Simple foo replacement' );
 68+ } );
 69+
 70+ it ( "should return $n if replacement not there", function() {
 71+ var parser = new mw.jqueryMsg.parser();
 72+ expect( parser.parse( 'en_replace', [] ).html() ).toEqual( 'Simple $1 replacement' );
 73+ expect( parser.parse( 'en_replace2', [ 'bar' ] ).html() ).toEqual( 'Simple bar $2 replacements' );
 74+ } );
 75+
 76+ } );
 77+
 78+ describe( "linking", function() {
 79+
 80+ it ( "should handle a simple link", function() {
 81+ var parser = new mw.jqueryMsg.parser();
 82+ var parsed = parser.parse( 'en_link' );
 83+ var contents = parsed.contents();
 84+ expect( contents.length ).toEqual( 3 );
 85+ expect( contents[0].nodeName ).toEqual( '#text' );
 86+ expect( contents[0].nodeValue ).toEqual( 'Simple ' );
 87+ expect( contents[1].nodeName ).toEqual( 'A' );
 88+ expect( contents[1].getAttribute( 'href' ) ).toEqual( 'http://example.com' );
 89+ expect( contents[1].childNodes[0].nodeValue ).toEqual( 'link to example' );
 90+ expect( contents[2].nodeName ).toEqual( '#text' );
 91+ expect( contents[2].nodeValue ).toEqual( '.' );
 92+ } );
 93+
 94+ it ( "should replace a URL into a link", function() {
 95+ var parser = new mw.jqueryMsg.parser();
 96+ var parsed = parser.parse( 'en_link_replace', [ 'http://example.com/foo', 'linking' ] );
 97+ var contents = parsed.contents();
 98+ expect( contents.length ).toEqual( 3 );
 99+ expect( contents[0].nodeName ).toEqual( '#text' );
 100+ expect( contents[0].nodeValue ).toEqual( 'Complex ' );
 101+ expect( contents[1].nodeName ).toEqual( 'A' );
 102+ expect( contents[1].getAttribute( 'href' ) ).toEqual( 'http://example.com/foo' );
 103+ expect( contents[1].childNodes[0].nodeValue ).toEqual( 'linking' );
 104+ expect( contents[2].nodeName ).toEqual( '#text' );
 105+ expect( contents[2].nodeValue ).toEqual( ' behaviour.' );
 106+ } );
 107+
 108+ it ( "should bind a click handler into a link", function() {
 109+ var parser = new mw.jqueryMsg.parser();
 110+ var clicked = false;
 111+ var click = function() { clicked = true; };
 112+ var parsed = parser.parse( 'en_link_replace', [ click, 'linking' ] );
 113+ var contents = parsed.contents();
 114+ expect( contents.length ).toEqual( 3 );
 115+ expect( contents[0].nodeName ).toEqual( '#text' );
 116+ expect( contents[0].nodeValue ).toEqual( 'Complex ' );
 117+ expect( contents[1].nodeName ).toEqual( 'A' );
 118+ expect( contents[1].getAttribute( 'href' ) ).toEqual( '#' );
 119+ expect( contents[1].childNodes[0].nodeValue ).toEqual( 'linking' );
 120+ expect( contents[2].nodeName ).toEqual( '#text' );
 121+ expect( contents[2].nodeValue ).toEqual( ' behaviour.' );
 122+ // determining bindings is hard in IE
 123+ var anchor = parsed.find( 'a' );
 124+ if ( ( $.browser.mozilla || $.browser.webkit ) && anchor.click ) {
 125+ expect( clicked ).toEqual( false );
 126+ anchor.click();
 127+ expect( clicked ).toEqual( true );
 128+ }
 129+ } );
 130+
 131+ it ( "should wrap a jquery arg around link contents -- even another element", function() {
 132+ var parser = new mw.jqueryMsg.parser();
 133+ var clicked = false;
 134+ var click = function() { clicked = true; };
 135+ var button = $( '<button>' ).click( click );
 136+ var parsed = parser.parse( 'en_link_replace', [ button, 'buttoning' ] );
 137+ var contents = parsed.contents();
 138+ expect( contents.length ).toEqual( 3 );
 139+ expect( contents[0].nodeName ).toEqual( '#text' );
 140+ expect( contents[0].nodeValue ).toEqual( 'Complex ' );
 141+ expect( contents[1].nodeName ).toEqual( 'BUTTON' );
 142+ expect( contents[1].childNodes[0].nodeValue ).toEqual( 'buttoning' );
 143+ expect( contents[2].nodeName ).toEqual( '#text' );
 144+ expect( contents[2].nodeValue ).toEqual( ' behaviour.' );
 145+ // determining bindings is hard in IE
 146+ if ( ( $.browser.mozilla || $.browser.webkit ) && button.click ) {
 147+ expect( clicked ).toEqual( false );
 148+ parsed.find( 'button' ).click();
 149+ expect( clicked ).toEqual( true );
 150+ }
 151+ } );
 152+
 153+
 154+ } );
 155+
 156+
 157+ describe( "magic keywords", function() {
 158+ it( "should substitute magic keywords", function() {
 159+ var options = {
 160+ magic: {
 161+ 'alohomora' : 'open'
 162+ }
 163+ };
 164+ var parser = new mw.jqueryMsg.parser( options );
 165+ expect( parser.parse( 'en_simple_magic' ).html() ).toEqual( 'Simple open message' );
 166+ } );
 167+ } );
 168+
 169+ describe( "error conditions", function() {
 170+ it( "should return non-existent key in square brackets", function() {
 171+ var parser = new mw.jqueryMsg.parser();
 172+ expect( parser.parse( 'en_does_not_exist' ).html() ).toEqual( '[en_does_not_exist]' );
 173+ } );
 174+
 175+
 176+ it( "should fail to parse", function() {
 177+ var parser = new mw.jqueryMsg.parser();
 178+ expect( function() { parser.parse( 'en_fail' ); } ).toThrow(
 179+ 'Parse error at position 20 in input: This should fail to {{parse'
 180+ );
 181+ } );
 182+ } );
 183+
 184+ describe( "empty parameters", function() {
 185+ it( "should deal with empty parameters", function() {
 186+ var parser = new mw.jqueryMsg.parser();
 187+ var ast = parser.getAst( 'en_undelete_empty_param' );
 188+ expect( parser.parse( 'en_undelete_empty_param', [ 1 ] ).html() ).toEqual( 'Undelete' );
 189+ expect( parser.parse( 'en_undelete_empty_param', [ 3 ] ).html() ).toEqual( 'Undelete multiple edits' );
 190+
 191+ } );
 192+ } );
 193+
 194+ describe( "easy message interface functions", function() {
 195+ it( "should allow a global that returns strings", function() {
 196+ var gM = mw.jqueryMsg.getMessageFunction();
 197+ // passing this through jQuery and back to string, because browsers may have subtle differences, like the case of tag names.
 198+ // a surrounding <SPAN> is needed for html() to work right
 199+ var expectedHtml = $( '<span>Complex <a href="http://example.com/foo">linking</a> behaviour.</span>' ).html();
 200+ var result = gM( 'en_link_replace', 'http://example.com/foo', 'linking' );
 201+ expect( typeof result ).toEqual( 'string' );
 202+ expect( result ).toEqual( expectedHtml );
 203+ } );
 204+
 205+ it( "should allow a jQuery plugin that appends to nodes", function() {
 206+ $.fn.msg = mw.jqueryMsg.getPlugin();
 207+ var $div = $( '<div>' ).append( $( '<p>' ).addClass( 'foo' ) );
 208+ var clicked = false;
 209+ var $button = $( '<button>' ).click( function() { clicked = true; } );
 210+ $div.find( '.foo' ).msg( 'en_link_replace', $button, 'buttoning' );
 211+ // passing this through jQuery and back to string, because browsers may have subtle differences, like the case of tag names.
 212+ // a surrounding <SPAN> is needed for html() to work right
 213+ var expectedHtml = $( '<span>Complex <button>buttoning</button> behaviour.</span>' ).html();
 214+ var createdHtml = $div.find( '.foo' ).html();
 215+ // it is hard to test for clicks with IE; also it inserts or removes spaces around nodes when creating HTML tags, depending on their type.
 216+ // so need to check the strings stripped of spaces.
 217+ if ( ( $.browser.mozilla || $.browser.webkit ) && $button.click ) {
 218+ expect( createdHtml ).toEqual( expectedHtml );
 219+ $div.find( 'button ').click();
 220+ expect( clicked ).toEqual( true );
 221+ } else if ( $.browser.ie ) {
 222+ expect( createdHtml.replace( /\s/, '' ) ).toEqual( expectedHtml.replace( /\s/, '' ) );
 223+ }
 224+ delete $.fn.msg;
 225+ } );
 226+
 227+ } );
 228+
 229+ // The parser functions can throw errors, but let's not actually blow up for the user -- instead dump the error into the interface so we have
 230+ // a chance at fixing this
 231+ describe( "easy message interface functions with graceful failures", function() {
 232+ it( "should allow a global that returns strings, with graceful failure", function() {
 233+ var gM = mw.jqueryMsg.getMessageFunction();
 234+ // passing this through jQuery and back to string, because browsers may have subtle differences, like the case of tag names.
 235+ // a surrounding <SPAN> is needed for html() to work right
 236+ var expectedHtml = $( '<span>en_fail: Parse error at position 20 in input: This should fail to {{parse</span>' ).html();
 237+ var result = gM( 'en_fail' );
 238+ expect( typeof result ).toEqual( 'string' );
 239+ expect( result ).toEqual( expectedHtml );
 240+ } );
 241+
 242+ it( "should allow a global that returns strings, with graceful failure on missing magic words", function() {
 243+ var gM = mw.jqueryMsg.getMessageFunction();
 244+ // passing this through jQuery and back to string, because browsers may have subtle differences, like the case of tag names.
 245+ // a surrounding <SPAN> is needed for html() to work right
 246+ var expectedHtml = $( '<span>en_fail_magic: unknown operation "sietname"</span>' ).html();
 247+ var result = gM( 'en_fail_magic' );
 248+ expect( typeof result ).toEqual( 'string' );
 249+ expect( result ).toEqual( expectedHtml );
 250+ } );
 251+
 252+
 253+ it( "should allow a jQuery plugin, with graceful failure", function() {
 254+ $.fn.msg = mw.jqueryMsg.getPlugin();
 255+ var $div = $( '<div>' ).append( $( '<p>' ).addClass( 'foo' ) );
 256+ $div.find( '.foo' ).msg( 'en_fail' );
 257+ // passing this through jQuery and back to string, because browsers may have subtle differences, like the case of tag names.
 258+ // a surrounding <SPAN> is needed for html() to work right
 259+ var expectedHtml = $( '<span>en_fail: Parse error at position 20 in input: This should fail to {{parse</span>' ).html();
 260+ var createdHtml = $div.find( '.foo' ).html();
 261+ expect( createdHtml ).toEqual( expectedHtml );
 262+ delete $.fn.msg;
 263+ } );
 264+
 265+ } );
 266+
 267+
 268+
 269+
 270+ describe( "test plurals and other language-specific functions", function() {
 271+ /* copying some language definitions in here -- it's hard to make this test fast and reliable
 272+ otherwise, and we don't want to have to know the mediawiki URL from this kind of test either.
 273+ We also can't preload the langs for the test since they clobber the same namespace.
 274+ In principle Roan said it was okay to change how languages worked so that didn't happen... maybe
 275+ someday. We'd have to the same kind of importing of the default rules for most rules, or maybe
 276+ come up with some kind of subclassing scheme for languages */
 277+ var languageClasses = {
 278+ ar: {
 279+ /**
 280+ * Arabic (العربية) language functions
 281+ */
 282+
 283+ convertPlural: function( count, forms ) {
 284+ forms = mw.language.preConvertPlural( forms, 6 );
 285+ if ( count === 0 ) {
 286+ return forms[0];
 287+ }
 288+ if ( count == 1 ) {
 289+ return forms[1];
 290+ }
 291+ if ( count == 2 ) {
 292+ return forms[2];
 293+ }
 294+ if ( count % 100 >= 3 && count % 100 <= 10 ) {
 295+ return forms[3];
 296+ }
 297+ if ( count % 100 >= 11 && count % 100 <= 99 ) {
 298+ return forms[4];
 299+ }
 300+ return forms[5];
 301+ },
 302+
 303+ digitTransformTable: {
 304+ '0': '٠', // &#x0660;
 305+ '1': '١', // &#x0661;
 306+ '2': '٢', // &#x0662;
 307+ '3': '٣', // &#x0663;
 308+ '4': '٤', // &#x0664;
 309+ '5': '٥', // &#x0665;
 310+ '6': '٦', // &#x0666;
 311+ '7': '٧', // &#x0667;
 312+ '8': '٨', // &#x0668;
 313+ '9': '٩', // &#x0669;
 314+ '.': '٫', // &#x066b; wrong table ?
 315+ ',': '٬' // &#x066c;
 316+ }
 317+
 318+ },
 319+ en: { },
 320+ fr: {
 321+ convertPlural: function( count, forms ) {
 322+ forms = mw.language.preConvertPlural( forms, 2 );
 323+ return ( count <= 1 ) ? forms[0] : forms[1];
 324+ }
 325+ },
 326+ jp: { },
 327+ zh: { }
 328+ };
 329+
 330+ /* simulate how the language classes override, or don't, the standard functions in mw.language */
 331+ $.each( languageClasses, function( langCode, rules ) {
 332+ $.each( [ 'convertPlural', 'convertNumber' ], function( i, propertyName ) {
 333+ if ( typeof rules[ propertyName ] === 'undefined' ) {
 334+ rules[ propertyName ] = mw.language[ propertyName ];
 335+ }
 336+ } );
 337+ } );
 338+
 339+ $.each( jasmineMsgSpec, function( i, test ) {
 340+ it( "should parse " + test.name, function() {
 341+ // using language override so we don't have to muck with global namespace
 342+ var parser = new mw.jqueryMsg.parser( { language: languageClasses[ test.lang ] } );
 343+ var parsedHtml = parser.parse( test.key, test.args ).html();
 344+ expect( parsedHtml ).toEqual( test.result );
 345+ } );
 346+ } );
 347+
 348+ } );
 349+
 350+ } );
 351+} )( window.mediaWiki, jQuery );
Property changes on: trunk/phase3/tests/jasmine/spec/mediawiki.jqueryMsg.spec.js
___________________________________________________________________
Added: svn:eol-style
1352 + native
Index: trunk/phase3/tests/jasmine/spec_makers/makeJqueryMsgSpec.php
@@ -0,0 +1,114 @@
 2+<?php
 3+
 4+/**
 5+ * This PHP script defines the spec that the Javascript message parser should conform to.
 6+ *
 7+ * It does this by looking up the results of various string kinds of string parsing, with various languages,
 8+ * in the current installation of MediaWiki. It then outputs a static specification, mapping expected inputs to outputs,
 9+ * which can be used with the JasmineBDD framework. This specification can then be used by simply including it into
 10+ * the SpecRunner.html file.
 11+ *
 12+ * This is similar to Michael Dale (mdale@mediawiki.org)'s parser tests, except that it doesn't look up the
 13+ * API results while doing the test, so the Jasmine run is much faster(at the cost of being out of date in rare
 14+ * circumstances. But mostly the parsing that we are doing in Javascript doesn't change much.)
 15+ *
 16+ */
 17+
 18+$maintenanceDir = dirname( dirname( dirname( dirname( dirname( __FILE__ ) ) ) ) ) . '/maintenance';
 19+
 20+require( "$maintenanceDir/Maintenance.php" );
 21+
 22+class MakeLanguageSpec extends Maintenance {
 23+
 24+ static $keyToTestArgs = array(
 25+ 'undelete_short' => array(
 26+ array( 0 ),
 27+ array( 1 ),
 28+ array( 2 ),
 29+ array( 5 ),
 30+ array( 21 ),
 31+ array( 101 )
 32+ ),
 33+ 'category-subcat-count' => array(
 34+ array( 0, 10 ),
 35+ array( 1, 1 ),
 36+ array( 1, 2 ),
 37+ array( 3, 30 )
 38+ )
 39+ );
 40+
 41+ public function __construct() {
 42+ parent::__construct();
 43+ $this->mDescription = "Create a JasmineBDD-compatible specification for message parsing";
 44+ // add any other options here
 45+ }
 46+
 47+ public function execute() {
 48+ list( $messages, $tests ) = $this->getMessagesAndTests();
 49+ $this->writeJavascriptFile( $messages, $tests, "spec/mediawiki.language.parser.spec.data.js" );
 50+ }
 51+
 52+ private function getMessagesAndTests() {
 53+ $messages = array();
 54+ $tests = array();
 55+ $wfMsgExtOptions = array( 'parsemag' );
 56+ foreach ( array( 'en', 'fr', 'ar', 'jp', 'zh' ) as $languageCode ) {
 57+ $wfMsgExtOptions['language'] = $languageCode;
 58+ foreach ( self::$keyToTestArgs as $key => $testArgs ) {
 59+ foreach ($testArgs as $args) {
 60+ // get the raw template, without any transformations
 61+ $template = wfMsgGetKey( $key, /* useDb */ true, $languageCode, /* transform */ false );
 62+
 63+ // get the magic-parsed version with args
 64+ $wfMsgExtArgs = array_merge( array( $key, $wfMsgExtOptions ), $args );
 65+ $result = call_user_func_array( 'wfMsgExt', $wfMsgExtArgs );
 66+
 67+ // record the template, args, language, and expected result
 68+ // fake multiple languages by flattening them together
 69+ $langKey = $languageCode . '_' . $key;
 70+ $messages[ $langKey ] = $template;
 71+ $tests[] = array(
 72+ 'name' => $languageCode . " " . $key . " " . join( ",", $args ),
 73+ 'key' => $langKey,
 74+ 'args' => $args,
 75+ 'result' => $result,
 76+ 'lang' => $languageCode
 77+ );
 78+ }
 79+ }
 80+ }
 81+ return array( $messages, $tests );
 82+ }
 83+
 84+ private function writeJavascriptFile( $messages, $tests, $dataSpecFile ) {
 85+ global $argv;
 86+ $arguments = count($argv) ? $argv : $_SERVER[ 'argv' ];
 87+
 88+ $json = new Services_JSON;
 89+ $json->pretty = true;
 90+ $javascriptPrologue = "// This file stores the results from the PHP parser for certain messages and arguments,\n"
 91+ . "// so we can test the equivalent Javascript libraries.\n"
 92+ . '// Last generated with ' . join(' ', $arguments) . ' at ' . gmdate('c') . "\n\n";
 93+ $javascriptMessages = "mediaWiki.messages.set( " . $json->encode( $messages, true ) . " );\n";
 94+ $javascriptTests = 'var jasmineMsgSpec = ' . $json->encode( $tests, true ) . ";\n";
 95+
 96+ $fp = fopen( $dataSpecFile, 'w' );
 97+ if ( !$fp ) {
 98+ die( "couldn't open $dataSpecFile for writing" );
 99+ }
 100+ $success = fwrite( $fp, $javascriptPrologue . $javascriptMessages . $javascriptTests );
 101+ if ( !$success ) {
 102+ die( "couldn't write to $dataSpecFile" );
 103+ }
 104+ $success = fclose( $fp );
 105+ if ( !$success ) {
 106+ die( "couldn't close $dataSpecFile" );
 107+ }
 108+ }
 109+}
 110+
 111+$maintClass = "MakeLanguageSpec";
 112+require_once( "$maintenanceDir/doMaintenance.php" );
 113+
 114+
 115+
Property changes on: trunk/phase3/tests/jasmine/spec_makers/makeJqueryMsgSpec.php
___________________________________________________________________
Added: svn:eol-style
1116 + native
Index: trunk/phase3/resources/mediawiki/mediawiki.jqueryMsg.peg
@@ -0,0 +1,76 @@
 2+/* PEG grammar for a subset of wikitext, useful in the MediaWiki frontend */
 3+
 4+start
 5+ = e:expression* { return e.length > 1 ? [ "CONCAT" ].concat(e) : e[0]; }
 6+
 7+expression
 8+ = template
 9+ / link
 10+ / extlink
 11+ / replacement
 12+ / literal
 13+
 14+paramExpression
 15+ = template
 16+ / link
 17+ / extlink
 18+ / replacement
 19+ / literalWithoutBar
 20+
 21+template
 22+ = "{{" t:templateContents "}}" { return t; }
 23+
 24+templateContents
 25+ = twr:templateWithReplacement p:templateParam* { return twr.concat(p) }
 26+ / t:templateName p:templateParam* { return p.length ? [ t, p ] : [ t ] }
 27+
 28+templateWithReplacement
 29+ = t:templateName ":" r:replacement { return [ t, r ] }
 30+
 31+templateParam
 32+ = "|" e:paramExpression* { return e.length > 1 ? [ "CONCAT" ].concat(e) : e[0]; }
 33+
 34+templateName
 35+ = tn:[A-Za-z_]+ { return tn.join('').toUpperCase() }
 36+
 37+link
 38+ = "[[" w:expression "]]" { return [ 'WLINK', w ]; }
 39+
 40+extlink
 41+ = "[" url:url whitespace text:expression "]" { return [ 'LINK', url, text ] }
 42+
 43+url
 44+ = url:[^ ]+ { return url.join(''); }
 45+
 46+whitespace
 47+ = [ ]+
 48+
 49+replacement
 50+ = '$' digits:digits { return [ 'REPLACE', parseInt( digits, 10 ) - 1 ] }
 51+
 52+digits
 53+ = [0-9]+
 54+
 55+literal
 56+ = lit:escapedOrRegularLiteral+ { return lit.join(''); }
 57+
 58+literalWithoutBar
 59+ = lit:escapedOrLiteralWithoutBar+ { return lit.join(''); }
 60+
 61+escapedOrRegularLiteral
 62+ = escapedLiteral
 63+ / regularLiteral
 64+
 65+escapedOrLiteralWithoutBar
 66+ = escapedLiteral
 67+ / regularLiteralWithoutBar
 68+
 69+escapedLiteral
 70+ = "\\" escaped:. { return escaped; }
 71+
 72+regularLiteral
 73+ = [^{}\[\]$\\]
 74+
 75+regularLiteralWithoutBar
 76+ = [^{}\[\]$\\|]
 77+
Index: trunk/phase3/resources/mediawiki/mediawiki.jqueryMsg.js
@@ -0,0 +1,648 @@
 2+/**
 3+ * Experimental advanced wikitext parser-emitter.
 4+ * See: http://www.mediawiki.org/wiki/Extension:UploadWizard/MessageParser for docs
 5+ *
 6+ * @author neilk@wikimedia.org
 7+ */
 8+
 9+( function( mw, $, undefined ) {
 10+
 11+ mw.jqueryMsg = {};
 12+
 13+ /**
 14+ * Given parser options, return a function that parses a key and replacements, returning jQuery object
 15+ * @param {Object} parser options
 16+ * @return {Function} accepting ( String message key, String replacement1, String replacement2 ... ) and returning {jQuery}
 17+ */
 18+ function getFailableParserFn( options ) {
 19+ var parser = new mw.jqueryMsg.parser( options );
 20+ /**
 21+ * Try to parse a key and optional replacements, returning a jQuery object that may be a tree of jQuery nodes.
 22+ * If there was an error parsing, return the key and the error message (wrapped in jQuery). This should put the error right into
 23+ * the interface, without causing the page to halt script execution, and it hopefully should be clearer how to fix it.
 24+ *
 25+ * @param {Array} first element is the key, replacements may be in array in 2nd element, or remaining elements.
 26+ * @return {jQuery}
 27+ */
 28+ return function( args ) {
 29+ var key = args[0];
 30+ var replacements = $.isArray( args[1] ) ? args[1] : $.makeArray( args ).slice( 1 );
 31+ try {
 32+ return parser.parse( key, replacements );
 33+ } catch ( e ) {
 34+ return $( '<span></span>' ).append( key + ': ' + e.message );
 35+ }
 36+ };
 37+ }
 38+
 39+ /**
 40+ * Class method.
 41+ * Returns a function suitable for use as a global, to construct strings from the message key (and optional replacements).
 42+ * e.g.
 43+ * window.gM = mediaWiki.parser.getMessageFunction( options );
 44+ * $( 'p#headline' ).html( gM( 'hello-user', username ) );
 45+ *
 46+ * Like the old gM() function this returns only strings, so it destroys any bindings. If you want to preserve bindings use the
 47+ * jQuery plugin version instead. This is only included for backwards compatibility with gM().
 48+ *
 49+ * @param {Array} parser options
 50+ * @return {Function} function suitable for assigning to window.gM
 51+ */
 52+ mw.jqueryMsg.getMessageFunction = function( options ) {
 53+ var failableParserFn = getFailableParserFn( options );
 54+ /**
 55+ * N.B. replacements are variadic arguments or an array in second parameter. In other words:
 56+ * somefunction(a, b, c, d)
 57+ * is equivalent to
 58+ * somefunction(a, [b, c, d])
 59+ *
 60+ * @param {String} message key
 61+ * @param {Array} optional replacements (can also specify variadically)
 62+ * @return {String} rendered HTML as string
 63+ */
 64+ return function( /* key, replacements */ ) {
 65+ return failableParserFn( arguments ).html();
 66+ };
 67+ };
 68+
 69+ /**
 70+ * Class method.
 71+ * Returns a jQuery plugin which parses the message in the message key, doing replacements optionally, and appends the nodes to
 72+ * the current selector. Bindings to passed-in jquery elements are preserved. Functions become click handlers for [$1 linktext] links.
 73+ * e.g.
 74+ * $.fn.msg = mediaWiki.parser.getJqueryPlugin( options );
 75+ * var userlink = $( '<a>' ).click( function() { alert( "hello!!") } );
 76+ * $( 'p#headline' ).msg( 'hello-user', userlink );
 77+ *
 78+ * @param {Array} parser options
 79+ * @return {Function} function suitable for assigning to jQuery plugin, such as $.fn.msg
 80+ */
 81+ mw.jqueryMsg.getPlugin = function( options ) {
 82+ var failableParserFn = getFailableParserFn( options );
 83+ /**
 84+ * N.B. replacements are variadic arguments or an array in second parameter. In other words:
 85+ * somefunction(a, b, c, d)
 86+ * is equivalent to
 87+ * somefunction(a, [b, c, d])
 88+ *
 89+ * We append to 'this', which in a jQuery plugin context will be the selected elements.
 90+ * @param {String} message key
 91+ * @param {Array} optional replacements (can also specify variadically)
 92+ * @return {jQuery} this
 93+ */
 94+ return function( /* key, replacements */ ) {
 95+ var $target = this.empty();
 96+ $.each( failableParserFn( arguments ).contents(), function( i, node ) {
 97+ $target.append( node );
 98+ } );
 99+ return $target;
 100+ };
 101+ };
 102+
 103+ var parserDefaults = {
 104+ 'magic' : {},
 105+ 'messages' : mw.messages,
 106+ 'language' : mw.language
 107+ };
 108+
 109+ /**
 110+ * The parser itself.
 111+ * Describes an object, whose primary duty is to .parse() message keys.
 112+ * @param {Array} options
 113+ */
 114+ mw.jqueryMsg.parser = function( options ) {
 115+ this.settings = $.extend( {}, parserDefaults, options );
 116+ this.emitter = new mw.jqueryMsg.htmlEmitter( this.settings.language, this.settings.magic );
 117+ };
 118+
 119+ mw.jqueryMsg.parser.prototype = {
 120+
 121+ // cache, map of mediaWiki message key to the AST of the message. In most cases, the message is a string so this is identical.
 122+ // (This is why we would like to move this functionality server-side).
 123+ astCache: {},
 124+
 125+ /**
 126+ * Where the magic happens.
 127+ * Parses a message from the key, and swaps in replacements as necessary, wraps in jQuery
 128+ * If an error is thrown, returns original key, and logs the error
 129+ * @param {String} message key
 130+ * @param {Array} replacements for $1, $2... $n
 131+ * @return {jQuery}
 132+ */
 133+ parse: function( key, replacements ) {
 134+ return this.emitter.emit( this.getAst( key ), replacements );
 135+ },
 136+
 137+ /**
 138+ * Fetch the message string associated with a key, return parsed structure. Memoized.
 139+ * Note that we pass '[' + key + ']' back for a missing message here.
 140+ * @param {String} key
 141+ * @return {String|Array} string of '[key]' if message missing, simple string if possible, array of arrays if needs parsing
 142+ */
 143+ getAst: function( key ) {
 144+ if ( this.astCache[ key ] === undefined ) {
 145+ var wikiText = this.settings.messages.get( key );
 146+ if ( typeof wikiText !== 'string' ) {
 147+ wikiText = "\\[" + key + "\\]";
 148+ }
 149+ this.astCache[ key ] = this.wikiTextToAst( wikiText );
 150+ }
 151+ return this.astCache[ key ];
 152+ },
 153+
 154+ /*
 155+ * Parses the input wikiText into an abstract syntax tree, essentially an s-expression.
 156+ *
 157+ * CAVEAT: This does not parse all wikitext. It could be more efficient, but it's pretty good already.
 158+ * n.b. We want to move this functionality to the server. Nothing here is required to be on the client.
 159+ *
 160+ * @param {String} message string wikitext
 161+ * @throws Error
 162+ * @return {Mixed} abstract syntax tree
 163+ */
 164+ wikiTextToAst: function( input ) {
 165+
 166+ // Indicates current position in input as we parse through it.
 167+ // Shared among all parsing functions below.
 168+ var pos = 0;
 169+
 170+ // =========================================================
 171+ // parsing combinators - could be a library on its own
 172+ // =========================================================
 173+
 174+
 175+ // Try parsers until one works, if none work return null
 176+ function choice( ps ) {
 177+ return function() {
 178+ for ( var i = 0; i < ps.length; i++ ) {
 179+ var result = ps[i]();
 180+ if ( result !== null ) {
 181+ return result;
 182+ }
 183+ }
 184+ return null;
 185+ };
 186+ }
 187+
 188+ // try several ps in a row, all must succeed or return null
 189+ // this is the only eager one
 190+ function sequence( ps ) {
 191+ var originalPos = pos;
 192+ var result = [];
 193+ for ( var i = 0; i < ps.length; i++ ) {
 194+ var res = ps[i]();
 195+ if ( res === null ) {
 196+ pos = originalPos;
 197+ return null;
 198+ }
 199+ result.push( res );
 200+ }
 201+ return result;
 202+ }
 203+
 204+ // run the same parser over and over until it fails.
 205+ // must succeed a minimum of n times or return null
 206+ function nOrMore( n, p ) {
 207+ return function() {
 208+ var originalPos = pos;
 209+ var result = [];
 210+ var parsed = p();
 211+ while ( parsed !== null ) {
 212+ result.push( parsed );
 213+ parsed = p();
 214+ }
 215+ if ( result.length < n ) {
 216+ pos = originalPos;
 217+ return null;
 218+ }
 219+ return result;
 220+ };
 221+ }
 222+
 223+ // There is a general pattern -- parse a thing, if that worked, apply transform, otherwise return null.
 224+ // But using this as a combinator seems to cause problems when combined with nOrMore().
 225+ // May be some scoping issue
 226+ function transform( p, fn ) {
 227+ return function() {
 228+ var result = p();
 229+ return result === null ? null : fn( result );
 230+ };
 231+ }
 232+
 233+ // Helpers -- just make ps out of simpler JS builtin types
 234+
 235+ function makeStringParser( s ) {
 236+ var len = s.length;
 237+ return function() {
 238+ var result = null;
 239+ if ( input.substr( pos, len ) === s ) {
 240+ result = s;
 241+ pos += len;
 242+ }
 243+ return result;
 244+ };
 245+ }
 246+
 247+ function makeRegexParser( regex ) {
 248+ return function() {
 249+ var matches = input.substr( pos ).match( regex );
 250+ if ( matches === null ) {
 251+ return null;
 252+ }
 253+ pos += matches[0].length;
 254+ return matches[0];
 255+ };
 256+ }
 257+
 258+
 259+ /**
 260+ * ===================================================================
 261+ * General patterns above this line -- wikitext specific parsers below
 262+ * ===================================================================
 263+ */
 264+
 265+ // Parsing functions follow. All parsing functions work like this:
 266+ // They don't accept any arguments.
 267+ // Instead, they just operate non destructively on the string 'input'
 268+ // As they can consume parts of the string, they advance the shared variable pos,
 269+ // and return tokens (or whatever else they want to return).
 270+
 271+ // some things are defined as closures and other things as ordinary functions
 272+ // converting everything to a closure makes it a lot harder to debug... errors pop up
 273+ // but some debuggers can't tell you exactly where they come from. Also the mutually
 274+ // recursive functions seem not to work in all browsers then. (Tested IE6-7, Opera, Safari, FF)
 275+ // This may be because, to save code, memoization was removed
 276+
 277+
 278+ var regularLiteral = makeRegexParser( /^[^{}[\]$\\]/ );
 279+ var regularLiteralWithoutBar = makeRegexParser(/^[^{}[\]$\\|]/);
 280+ var regularLiteralWithoutSpace = makeRegexParser(/^[^{}[\]$\s]/);
 281+
 282+ var backslash = makeStringParser( "\\" );
 283+ var anyCharacter = makeRegexParser( /^./ );
 284+
 285+ function escapedLiteral() {
 286+ var result = sequence( [
 287+ backslash,
 288+ anyCharacter
 289+ ] );
 290+ return result === null ? null : result[1];
 291+ }
 292+
 293+ var escapedOrLiteralWithoutSpace = choice( [
 294+ escapedLiteral,
 295+ regularLiteralWithoutSpace
 296+ ] );
 297+
 298+ var escapedOrLiteralWithoutBar = choice( [
 299+ escapedLiteral,
 300+ regularLiteralWithoutBar
 301+ ] );
 302+
 303+ var escapedOrRegularLiteral = choice( [
 304+ escapedLiteral,
 305+ regularLiteral
 306+ ] );
 307+
 308+ // Used to define "literals" without spaces, in space-delimited situations
 309+ function literalWithoutSpace() {
 310+ var result = nOrMore( 1, escapedOrLiteralWithoutSpace )();
 311+ return result === null ? null : result.join('');
 312+ }
 313+
 314+ // Used to define "literals" within template parameters. The pipe character is the parameter delimeter, so by default
 315+ // it is not a literal in the parameter
 316+ function literalWithoutBar() {
 317+ var result = nOrMore( 1, escapedOrLiteralWithoutBar )();
 318+ return result === null ? null : result.join('');
 319+ }
 320+
 321+ function literal() {
 322+ var result = nOrMore( 1, escapedOrRegularLiteral )();
 323+ return result === null ? null : result.join('');
 324+ }
 325+
 326+ var whitespace = makeRegexParser( /^\s+/ );
 327+ var dollar = makeStringParser( '$' );
 328+ var digits = makeRegexParser( /^\d+/ );
 329+
 330+ function replacement() {
 331+ var result = sequence( [
 332+ dollar,
 333+ digits
 334+ ] );
 335+ if ( result === null ) {
 336+ return null;
 337+ }
 338+ return [ 'REPLACE', parseInt( result[1], 10 ) - 1 ];
 339+ }
 340+
 341+
 342+ var openExtlink = makeStringParser( '[' );
 343+ var closeExtlink = makeStringParser( ']' );
 344+
 345+ // this extlink MUST have inner text, e.g. [foo] not allowed; [foo bar] is allowed
 346+ function extlink() {
 347+ var result = null;
 348+ var parsedResult = sequence( [
 349+ openExtlink,
 350+ nonWhitespaceExpression,
 351+ whitespace,
 352+ expression,
 353+ closeExtlink
 354+ ] );
 355+ if ( parsedResult !== null ) {
 356+ result = [ 'LINK', parsedResult[1], parsedResult[3] ];
 357+ }
 358+ return result;
 359+ }
 360+
 361+ var openLink = makeStringParser( '[[' );
 362+ var closeLink = makeStringParser( ']]' );
 363+
 364+ function link() {
 365+ var result = null;
 366+ var parsedResult = sequence( [
 367+ openLink,
 368+ expression,
 369+ closeLink
 370+ ] );
 371+ if ( parsedResult !== null ) {
 372+ result = [ 'WLINK', parsedResult[1] ];
 373+ }
 374+ return result;
 375+ }
 376+
 377+ var templateName = transform(
 378+ // see $wgLegalTitleChars
 379+ // not allowing : due to the need to catch "PLURAL:$1"
 380+ makeRegexParser( /^[ !"$&'()*,.\/0-9;=?@A-Z\^_`a-z~\x80-\xFF+-]+/ ),
 381+ function( result ) { return result.toString(); }
 382+ );
 383+
 384+ function templateParam() {
 385+ var result = sequence( [
 386+ pipe,
 387+ nOrMore( 0, paramExpression )
 388+ ] );
 389+ if ( result === null ) {
 390+ return null;
 391+ }
 392+ var expr = result[1];
 393+ // use a "CONCAT" operator if there are multiple nodes, otherwise return the first node, raw.
 394+ return expr.length > 1 ? [ "CONCAT" ].concat( expr ) : expr[0];
 395+ }
 396+
 397+ var pipe = makeStringParser( '|' );
 398+
 399+ function templateWithReplacement() {
 400+ var result = sequence( [
 401+ templateName,
 402+ colon,
 403+ replacement
 404+ ] );
 405+ return result === null ? null : [ result[0], result[2] ];
 406+ }
 407+
 408+ var colon = makeStringParser(':');
 409+
 410+ var templateContents = choice( [
 411+ function() {
 412+ var res = sequence( [
 413+ templateWithReplacement,
 414+ nOrMore( 0, templateParam )
 415+ ] );
 416+ return res === null ? null : res[0].concat( res[1] );
 417+ },
 418+ function() {
 419+ var res = sequence( [
 420+ templateName,
 421+ nOrMore( 0, templateParam )
 422+ ] );
 423+ if ( res === null ) {
 424+ return null;
 425+ }
 426+ return [ res[0] ].concat( res[1] );
 427+ }
 428+ ] );
 429+
 430+ var openTemplate = makeStringParser('{{');
 431+ var closeTemplate = makeStringParser('}}');
 432+
 433+ function template() {
 434+ var result = sequence( [
 435+ openTemplate,
 436+ templateContents,
 437+ closeTemplate
 438+ ] );
 439+ return result === null ? null : result[1];
 440+ }
 441+
 442+ var nonWhitespaceExpression = choice( [
 443+ template,
 444+ link,
 445+ extlink,
 446+ replacement,
 447+ literalWithoutSpace
 448+ ] );
 449+
 450+ var paramExpression = choice( [
 451+ template,
 452+ link,
 453+ extlink,
 454+ replacement,
 455+ literalWithoutBar
 456+ ] );
 457+
 458+ var expression = choice( [
 459+ template,
 460+ link,
 461+ extlink,
 462+ replacement,
 463+ literal
 464+ ] );
 465+
 466+ function start() {
 467+ var result = nOrMore( 0, expression )();
 468+ if ( result === null ) {
 469+ return null;
 470+ }
 471+ return [ "CONCAT" ].concat( result );
 472+ }
 473+
 474+ // everything above this point is supposed to be stateless/static, but
 475+ // I am deferring the work of turning it into prototypes & objects. It's quite fast enough
 476+
 477+ // finally let's do some actual work...
 478+
 479+ var result = start();
 480+
 481+ /*
 482+ * For success, the p must have gotten to the end of the input
 483+ * and returned a non-null.
 484+ * n.b. This is part of language infrastructure, so we do not throw an internationalizable message.
 485+ */
 486+ if (result === null || pos !== input.length) {
 487+ throw new Error( "Parse error at position " + pos.toString() + " in input: " + input );
 488+ }
 489+ return result;
 490+ }
 491+
 492+ };
 493+
 494+ /**
 495+ * htmlEmitter - object which primarily exists to emit HTML from parser ASTs
 496+ */
 497+ mw.jqueryMsg.htmlEmitter = function( language, magic ) {
 498+ this.language = language;
 499+ var _this = this;
 500+
 501+ $.each( magic, function( key, val ) {
 502+ _this[ key.toLowerCase() ] = function() { return val; };
 503+ } );
 504+
 505+ /**
 506+ * (We put this method definition here, and not in prototype, to make sure it's not overwritten by any magic.)
 507+ * Walk entire node structure, applying replacements and template functions when appropriate
 508+ * @param {Mixed} abstract syntax tree (top node or subnode)
 509+ * @param {Array} replacements for $1, $2, ... $n
 510+ * @return {Mixed} single-string node or array of nodes suitable for jQuery appending
 511+ */
 512+ this.emit = function( node, replacements ) {
 513+ var ret = null;
 514+ var _this = this;
 515+ switch( typeof node ) {
 516+ case 'string':
 517+ case 'number':
 518+ ret = node;
 519+ break;
 520+ case 'object': // node is an array of nodes
 521+ var subnodes = $.map( node.slice( 1 ), function( n ) {
 522+ return _this.emit( n, replacements );
 523+ } );
 524+ var operation = node[0].toLowerCase();
 525+ if ( typeof _this[operation] === 'function' ) {
 526+ ret = _this[ operation ]( subnodes, replacements );
 527+ } else {
 528+ throw new Error( 'unknown operation "' + operation + '"' );
 529+ }
 530+ break;
 531+ case 'undefined':
 532+ // Parsing the empty string (as an entire expression, or as a paramExpression in a template) results in undefined
 533+ // Perhaps a more clever parser can detect this, and return the empty string? Or is that useful information?
 534+ // The logical thing is probably to return the empty string here when we encounter undefined.
 535+ ret = '';
 536+ break;
 537+ default:
 538+ throw new Error( 'unexpected type in AST: ' + typeof node );
 539+ }
 540+ return ret;
 541+ };
 542+
 543+ };
 544+
 545+ // For everything in input that follows double-open-curly braces, there should be an equivalent parser
 546+ // function. For instance {{PLURAL ... }} will be processed by 'plural'.
 547+ // If you have 'magic words' then configure the parser to have them upon creation.
 548+ //
 549+ // An emitter method takes the parent node, the array of subnodes and the array of replacements (the values that $1, $2... should translate to).
 550+ // Note: all such functions must be pure, with the exception of referring to other pure functions via this.language (convertPlural and so on)
 551+ mw.jqueryMsg.htmlEmitter.prototype = {
 552+
 553+ /**
 554+ * Parsing has been applied depth-first we can assume that all nodes here are single nodes
 555+ * Must return a single node to parents -- a jQuery with synthetic span
 556+ * However, unwrap any other synthetic spans in our children and pass them upwards
 557+ * @param {Array} nodes - mixed, some single nodes, some arrays of nodes
 558+ * @return {jQuery}
 559+ */
 560+ concat: function( nodes ) {
 561+ var span = $( '<span>' ).addClass( 'mediaWiki_htmlEmitter' );
 562+ $.each( nodes, function( i, node ) {
 563+ if ( node instanceof jQuery && node.hasClass( 'mediaWiki_htmlEmitter' ) ) {
 564+ $.each( node.contents(), function( j, childNode ) {
 565+ span.append( childNode );
 566+ } );
 567+ } else {
 568+ // strings, integers, anything else
 569+ span.append( node );
 570+ }
 571+ } );
 572+ return span;
 573+ },
 574+
 575+ /**
 576+ * Return replacement of correct index, or string if unavailable.
 577+ * Note that we expect the parsed parameter to be zero-based. i.e. $1 should have become [ 0 ].
 578+ * if the specified parameter is not found return the same string
 579+ * (e.g. "$99" -> parameter 98 -> not found -> return "$99" )
 580+ * TODO throw error if nodes.length > 1 ?
 581+ * @param {Array} of one element, integer, n >= 0
 582+ * @return {String} replacement
 583+ */
 584+ replace: function( nodes, replacements ) {
 585+ var index = parseInt( nodes[0], 10 );
 586+ return index < replacements.length ? replacements[index] : '$' + ( index + 1 );
 587+ },
 588+
 589+ /**
 590+ * Transform wiki-link
 591+ * TODO unimplemented
 592+ */
 593+ wlink: function( nodes ) {
 594+ return "unimplemented";
 595+ },
 596+
 597+ /**
 598+ * Transform parsed structure into external link
 599+ * If the href is a jQuery object, treat it as "enclosing" the link text.
 600+ * ... function, treat it as the click handler
 601+ * ... string, treat it as a URI
 602+ * TODO: throw an error if nodes.length > 2 ?
 603+ * @param {Array} of two elements, {jQuery|Function|String} and {String}
 604+ * @return {jQuery}
 605+ */
 606+ link: function( nodes ) {
 607+ var arg = nodes[0];
 608+ var contents = nodes[1];
 609+ var $el;
 610+ if ( arg instanceof jQuery ) {
 611+ $el = arg;
 612+ } else {
 613+ $el = $( '<a>' );
 614+ if ( typeof arg === 'function' ) {
 615+ $el.click( arg ).attr( 'href', '#' );
 616+ } else {
 617+ $el.attr( 'href', arg.toString() );
 618+ }
 619+ }
 620+ $el.append( contents );
 621+ return $el;
 622+ },
 623+
 624+ /**
 625+ * Transform parsed structure into pluralization
 626+ * n.b. The first node may be a non-integer (for instance, a string representing an Arabic number).
 627+ * So convert it back with the current language's convertNumber.
 628+ * @param {Array} of nodes, [ {String|Number}, {String}, {String} ... ]
 629+ * @return {String} selected pluralized form according to current language
 630+ */
 631+ plural: function( nodes ) {
 632+ var count = parseInt( this.language.convertNumber( nodes[0], true ), 10 );
 633+ var forms = nodes.slice(1);
 634+ return forms.length ? this.language.convertPlural( count, forms ) : '';
 635+ }
 636+
 637+ };
 638+
 639+ // TODO figure out a way to make magic work with common globals like wgSiteName, without requiring init from library users...
 640+ // var options = { magic: { 'SITENAME' : mw.config.get( 'wgSiteName' ) } };
 641+
 642+ // deprecated! don't rely on gM existing.
 643+ // the window.gM ought not to be required - or if required, not required here. But moving it to extensions breaks it (?!)
 644+ // Need to fix plugin so it could do attributes as well, then will be okay to remove this.
 645+ window.gM = mw.jqueryMsg.getMessageFunction();
 646+
 647+ $.fn.msg = mw.jqueryMsg.getPlugin();
 648+
 649+} )( mediaWiki, jQuery );
Property changes on: trunk/phase3/resources/mediawiki/mediawiki.jqueryMsg.js
___________________________________________________________________
Added: svn:eol-style
1650 + native

Follow-up revisions

RevisionCommit summaryAuthorDate
r106062merging extensions/VisualEditor....neilk18:47, 13 December 2011

Past revisions this follows-up on

RevisionCommit summaryAuthorDate
r105970add jquery messages to core, config mediawiki.feedback to use it correctlyneilk02:44, 13 December 2011
r105971moved message library that can parse limited wikitext, uses jquery, from Uplo...neilk02:44, 13 December 2011
r105973moved language library to core mediawiki.jqueryMsgneilk02:46, 13 December 2011

Comments

#Comment by Nikerabbit (talk | contribs)   06:01, 13 December 2011

Yay finally! I guess this still need to be integrated into the existing mw.message code?

#Comment by NeilK (talk | contribs)   07:09, 13 December 2011

should be a drop-in replacement. Not every library needs to use this, only if they want plurals (and eventually, grammar, gender, etc.)

#Comment by NeilK (talk | contribs)   07:11, 13 December 2011

or to have wikitext in their messages turned into jQuery thingies.

#Comment by Nikerabbit (talk | contribs)   08:06, 13 December 2011

It doesn't seem to have the same api. mw.message is what it is being used in the core already, so that's why I asked whether it can be integrated into it.

#Comment by Catrope (talk | contribs)   17:59, 13 December 2011

Are these verbatim copies of the files that were deleted in r105973? If so, they should be copied with preservation of history.

#Comment by NeilK (talk | contribs)   18:06, 13 December 2011

They are not quite verbatim. Some stuff was changed to match the new namespace, and some cleanups applied.

FWIW I asked Reedy about svn mv versus rm and add, he didn't seem to care much

Status & tagging log