Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion deploy/database/schema.game.sql
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ CREATE TABLE game (
last_winner_id SMALLINT UNSIGNED,
tournament_id SMALLINT UNSIGNED,
tournament_round_number SMALLINT UNSIGNED,
description VARCHAR(255) NOT NULL,
description VARCHAR(305) NOT NULL,
chat TEXT,
previous_game_id MEDIUMINT UNSIGNED,
FOREIGN KEY (previous_game_id) REFERENCES game(id)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE game MODIFY description VARCHAR(305) NOT NULL;
14 changes: 14 additions & 0 deletions src/api/DummyApiResponder.php
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,20 @@ protected function get_interface_response_loadGameData($args) {
);
}

protected function get_interface_response_loadTournaments($args) {
return $this->load_json_data_from_file(
'loadTournaments',
'noargs.json'
);
}

protected function get_interface_response_loadTournamentData($args) {
return $this->load_json_data_from_file(
'loadTournamentData',
$args['tournament'] . '.json'
);
}

protected function get_interface_response_countPendingGames() {
return $this->load_json_data_from_file(
'countPendingGames',
Expand Down
12 changes: 8 additions & 4 deletions src/engine/BMInterfaceTournament.php
Original file line number Diff line number Diff line change
Expand Up @@ -503,17 +503,21 @@ protected function generate_new_games(BMTournament $tournament) {
array($gameData['buttonId1'], $gameData['buttonId2'])
);

$description = 'Round ' . $gameData['roundNumber'];
if ('' != $tournament->description) {
$description = $tournament->description . ' ' . $description;
$roundDescription = 'Tournament Round ' . $gameData['roundNumber'];
$tournDescription = $tournament->description;

if ('' == trim($tournDescription)) {
$tournDescription = $roundDescription;
} else {
$tournDescription = $tournDescription . ' • ' . $roundDescription;
}

$interfaceResponse = $this->game()->create_game_from_button_ids(
array($gameData['playerId1'], $gameData['playerId2']),
array($gameData['buttonId1'], $gameData['buttonId2']),
$buttonNames,
$tournament->gameMaxWins,
$description,
$tournDescription,
NULL,
0, // needs to be non-null, but also a non-player ID
TRUE,
Expand Down
159 changes: 151 additions & 8 deletions src/ui/js/Env.js
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,8 @@ Env.applyBbCodeToHtml = function(htmlToParse) {

var tagName;

htmlToParse = htmlToParse.replace(/\n/g, '<br>');

while (htmlToParse) {
var currentPattern = allStartTagsPattern;
if (tagStack.length !== 0) {
Expand All @@ -413,28 +415,35 @@ Env.applyBbCodeToHtml = function(htmlToParse) {
// should be greedy, so that nested tags work right
// (E.g., in '...blah[/quote] blah [/quote] blah', we want the first .*
// to end at the first [/quote], not the second)
currentPattern = '^(.*?)(?:' + currentPattern + ')(.*)$';
//
// Note that we are using [\\s\\S] instead of . so that we get
// multiline matching, see
// https://simonwillison.net/2004/Sep/20/newlines/
// and
// https://stackoverflow.com/questions/1979884/
// how-to-use-javascript-regex-over-multiple-lines#16119722
currentPattern = '^([\\s\\S]*?)(?:' + currentPattern + ')([\\s\\S]*)$';
// case-insensitive, multi-line
var regExp = new RegExp(currentPattern, 'im');

var match = htmlToParse.match(regExp);
if (match) {
var stuffBeforeTag = match[1];
var matchStr = htmlToParse.match(regExp);
if (matchStr) {
var stuffBeforeTag = matchStr[1];
// javascript apparently believes that capture groups that don't
// match anything are just important as those that do. So we need
// to do some acrobatics to find the ones we actually care about.
// (match[0] is the whole matched string; match[1] is the stuff before
// the tag. So we start with match[2].)
tagName = '';
for (var i = 2; i < match.length; i++) {
tagName = match[i];
for (var i = 2; i < matchStr.length; i++) {
tagName = matchStr[i];
if (tagName) {
break;
}
}
tagName = tagName.toLowerCase();
var tagParameter = match[i + 1] || '';
var stuffAfterTag = match[match.length - 1];
var tagParameter = matchStr[i + 1] || '';
var stuffAfterTag = matchStr[matchStr.length - 1];

outputHtml += stuffBeforeTag;
if (tagName.substring(0, 1) === '/') {
Expand Down Expand Up @@ -524,6 +533,140 @@ Env.applyBbCodeToHtml = function(htmlToParse) {
return outputHtml;
};

Env.removeBbCodeFromHtml = function(htmlToParse) {

@cgolubi1 cgolubi1 Jun 28, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, now that i think about this with slightly fresh eyes:

  • I don't think i have a problem with the approach of implementing Env.removeBbCodeFromHtml
  • The concern i have is that we now have this duplication of var replacements, and if someone adds a new type of markup to Env.applyBbCodeToHtml without adding it to Env.removeBbCodeFromHtml, it'll make a mess.

It seems like the least intrusive fix would be to move this replacements config outside of each function to one place in Env, and have it be one dict that defines an applyRules behavior and a removeRules behavior (i'm not wedded to those names at all), for each piece of markup syntax.

What do you think?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's very reasonable, I'll refactor out 'replacements' into its own entity.

// This is all rather more complicated than one might expect, but any attempt
// to parse BB code using simple regular expressions rather than tokenization
// is in the same family as parsing HTML with regular expressions, which
// summons Zalgo.
// (See: http://stackoverflow.com/
// questions/1732348/regex-match-open-tags-except-xhtml-self-contained-tags)

var replacements = {
'b': {},
'i': {},
'u': {},
's': {},
'code': {},
'spoiler': {},
'quote': {},
'game': {
'isAtomic': true,
},
'player': {
'isAtomic': true,
},
'button': {
'isAtomic': true,
},
'set': {
'isAtomic': true,
},
'tourn': {
'isAtomic': true,
},
'wiki': {
'isAtomic': true,
},
'issue': {
'isAtomic': true,
},
'forum': {},
'[': {
'isAtomic': true,
},
};

var outputHtml = '';
var tagStack = [];

// We want to build a pattern that we can use to identify any single
// BB code start tag
var allStartTagsPattern = '';
$.each(replacements, function(tagName) {
if (allStartTagsPattern !== '') {
allStartTagsPattern += '|';
}
// Matches, e.g., '[ b ]' or '[game = "123"]'
// The (?:... part means that we want parentheses around the whole
// thing (so we we can OR it together with other ones), but we don't
// want to capture the value of the whole thing as a group
allStartTagsPattern +=
'(?:\\[(' + Env.escapeRegexp(tagName) + ')(?:=([^\\]]*?))?])';
});

var tagName;

htmlToParse = htmlToParse.replace(/\n/g, ' ');

while (htmlToParse) {
var currentPattern = allStartTagsPattern;
if (tagStack.length !== 0) {
// The tag that was most recently opened
tagName = tagStack[tagStack.length - 1];
// Matches '[/i]' et al.
// (so that we can spot the end of the current tag as well)
currentPattern +=
'|(?:\\[(/' + Env.escapeRegexp(tagName) + ')])';
}
// The first group should be non-greedy (hence the ?), and the last one
// should be greedy, so that nested tags work right
// (E.g., in '...blah[/quote] blah [/quote] blah', we want the first .*
// to end at the first [/quote], not the second)
//
// Note that we are using [\\s\\S] instead of . so that we get
// multiline matching, see
// https://simonwillison.net/2004/Sep/20/newlines/
// and
// https://stackoverflow.com/questions/1979884/
// how-to-use-javascript-regex-over-multiple-lines#16119722
currentPattern = '([\\s\\S]*?)(?:' + currentPattern + ')([\\s\\S]*)';

// case-insensitive, multi-line
var regExp = new RegExp(currentPattern, 'im');
var matchStr = htmlToParse.match(regExp);

if (matchStr) {
var stuffBeforeTag = matchStr[1];
// javascript apparently believes that capture groups that don't
// match anything are just important as those that do. So we need
// to do some acrobatics to find the ones we actually care about.
// (match[0] is the whole matched string; match[1] is the stuff before
// the tag. So we start with match[2].)
tagName = '';
for (var i = 2; i < matchStr.length; i++) {
tagName = matchStr[i];
if (tagName) {
break;
}
}
tagName = tagName.toLowerCase();
var stuffAfterTag = matchStr[matchStr.length - 1];

outputHtml += stuffBeforeTag;
if (tagName.substring(0, 1) === '/') {
// If we've found our closing tag, we can finish the current tag and
// pop it off the stack
tagName = tagStack.pop();
} else {
if (!replacements[tagName].isAtomic) {
// If there's a closing tag coming along later, push this tag
// on the stack so we'll know we're waiting on it
tagStack.push(tagName);
}
}

htmlToParse = stuffAfterTag;
} else {
// If we don't find any more BB code tags that we're interested in,
// then we must have reached the end
outputHtml += htmlToParse;
htmlToParse = '';
}
}

return outputHtml;
};

Env.escapeRegexp = function(str) {
return str.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1');
};
Expand Down
2 changes: 1 addition & 1 deletion src/ui/js/Game.js
Original file line number Diff line number Diff line change
Expand Up @@ -1836,7 +1836,7 @@ Game.pageAddGameHeader = function(action_desc) {

if (Api.game.description) {
Game.page.append($('<div>', {
'text': Api.game.description,
'html': Env.applyBbCodeToHtml(Api.game.description),
'class': 'gameDescDisplay',
}));
}
Expand Down
8 changes: 5 additions & 3 deletions src/ui/js/Overview.js
Original file line number Diff line number Diff line change
Expand Up @@ -443,9 +443,11 @@ Overview.addScoreCol = function(gameRow, gameInfo) {

Overview.addDescCol = function(gameRow, description) {
var descText = '';
if (typeof(description) == 'string') {
descText = description.substring(0, 30) +
((description.length > 30) ? '...' : '');
if (typeof(description) === 'string') {
var descriptionNoMarkup = Env.removeBbCodeFromHtml(description);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding is that this diff is changing the behavior of the GAME table on the Overview page.

Is my understanding correct, or does this diff have something to do with tournament behavior?

@blackshadowshade blackshadowshade Jan 4, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your understanding is correct, we would be stripping all BBCode from descriptions of games on the Overview page.

@blackshadowshade blackshadowshade Jan 4, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The stripping is to avoid the problem where the Overview shows something like this:

[forum=1331,32731]Description ...

instead of

Description with extra words

@cgolubi1 cgolubi1 Jan 4, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your understanding is correct, we would be stripping all BBCode from descriptions of games on the Overview page.

Okay. I have two points here, which are basically about documenting the behavior changes in PRs to make them easier to review:

  1. I know this PR is WIP, but the description doesn't say word one about changing the behavior of the games table. The description of a PR should describe the player-visible changes that appear in the PR. The reason it takes me so long to review these PRs is that i (i think correctly) assume there are a ton of player-visible changes which are not called out in the description, so i have to read the code and reverse-engineer them all before having an opinion about whether they are good. The more the PR description says what the intended player-visible changes are, the more we can skip to the second part.
  2. The games table has worked this way for a long time. It's not a new problem that's being introduced by allowing BBCode in tournament descriptions. That's why i'm questioning the urgency of fixing what shows up in these overview tables --- because afaik it's not a new problem. If it's something people have complained about, that's great evidence to put in the description of the issue being resolved.

@blackshadowshade blackshadowshade Jan 4, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re 1.: I had thought that by doing things the way I am, I was attempting to preserve the status quo, namely that the Overview page shows the description of the game, instead of showing a whole pile of BBCode that is obscuring the description of the game. I had not considered the fact that the change would be player-visible in the case when there was BBCode in the game title because I have never seen a game description with BBCode in any of my games.

Re 2.: I would argue that this pull request without this adaptation to the Overview page is actually creating a new player-visible problem because I have never seen a game description with BBCode before this pull request, but I am likely to see one after this pull request.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very belatedly: i buy that, and i'm fine with this approach.


descText = descriptionNoMarkup.substring(0, 30) +
((descriptionNoMarkup.length > 30) ? '...' : '');
}
gameRow.append($('<td>', {
'class': 'gameDescDisplay',
Expand Down
38 changes: 28 additions & 10 deletions src/ui/js/Tournament.js
Original file line number Diff line number Diff line change
Expand Up @@ -166,13 +166,7 @@ Tournament.pageAddTournamentHeader = function() {
// bgcolor = Tournament.color.opponent;
// }

if (Api.tournament.description) {
Tournament.page.append($('<div>', {
'text': Api.tournament.description,
'class': 'gameDescDisplay',
}));
}

Tournament.pageAddTournamentDescription();
Tournament.page.append($('<br>'));

Tournament.pageAddTournamentInfo();
Expand Down Expand Up @@ -306,8 +300,23 @@ Tournament.formFollowTournament = function(e) {
Tournament.showLoggedInPage);
};

Tournament.pageAddTournamentDescription = function () {
if (Api.tournament.description) {
Tournament.page.append($('<div>', {
'id': 'tournament_desc',
'html': Env.applyBbCodeToHtml(Api.tournament.description),
'class': 'gameDescDisplay',
}));
}
};

Tournament.pageAddTournamentInfo = function () {
var infoDiv = $('<div>');
var infoDiv = $(
'<div>',
{
'id': 'tournament_info',
}
);
Tournament.page.append(infoDiv);

var tournamentTypePar = $('<p>', {
Expand Down Expand Up @@ -347,9 +356,18 @@ Tournament.pageAddWinnerInfo = function () {
var winnerDiv = $('<div>');
Tournament.page.append(winnerDiv);

var winnerIdx = Api.tournament.remainCountArray.findIndex(
function(x) {return (x > 0);}
var winnerIdx;
var isWinnerFound = Api.tournament.remainCountArray.some(
function(remainCount, idx) {
winnerIdx = idx;
return remainCount > 0;
}
);

if (!isWinnerFound) {
return;
}

var winnerPar = $('<p>', {
'class': 'winner_name',
'text': 'Winner: ' + Api.tournament.playerDataArray[winnerIdx].playerName
Expand Down
6 changes: 4 additions & 2 deletions src/ui/js/TournamentOverview.js
Original file line number Diff line number Diff line change
Expand Up @@ -284,8 +284,10 @@ TournamentOverview.addTypeCol = function(tournamentRow, tournamentInfo) {
TournamentOverview.addDescCol = function(tournamentRow, description) {
var descText = '';
if (typeof(description) === 'string') {
descText = description.substring(0, 30) +
((description.length > 30) ? '...' : '');
var descriptionNoMarkup = Env.removeBbCodeFromHtml(description);

descText = descriptionNoMarkup.substring(0, 30) +
((descriptionNoMarkup.length > 30) ? '...' : '');
}
tournamentRow.append($('<td>', {
'class': 'tournamentDescDisplay',
Expand Down
27 changes: 27 additions & 0 deletions test/src/api/responderTestFramework.php
Original file line number Diff line number Diff line change
Expand Up @@ -1511,6 +1511,33 @@ protected function verify_api_setChatVisibility($expMessage, $gameId, $private)
$this->cache_json_api_output('setChatVisibility', $fakeGameNumber, $retval);
}


protected function verify_api_loadTournaments($expData) {
$args = array(
'type' => 'loadTournaments',
);
$retval = $this->verify_api_success($args);

$fullData = $retval['data'];

// Now restrict the data to include only the information for Tournament 1,
// since this is the tournament that will be generated with an empty database.
//
// This slicing makes the test code less fragile when testing with an AMP stack
// since the database need not be deleted each time the tests run.
$tournamentIdx = array_search(1, $fullData['tournamentIdArray'], TRUE);
$this->assertEquals(FALSE, is_bool($tournamentIdx), 'Tournament 1 should exist');
Comment on lines +1523 to +1529

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a pattern we use in the existing responder tests? I'm worried about making tests brittle in a new way, but if you can point to where we're already doing this for some other test type, then i obviously don't have a problem with it here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm guessing that we probably do not do this elsewhere in the responder tests.

This is very much a hack to get things working on repeated testing with an already extant database where each run creates a new tournament.

I'm open to suggestions on how to do this better, I couldn't find anything easy when I was trying to get the tests passing.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha. I can take a more informed look at some point - mind adding a checklist item for it?

Anyway, i'm not going to block this PR over it if we get to the point where it's mergeable, i don't think. But i want to at least think about it a little more, if not enough to make a suggestion.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added a point to the checklist for this.


$slicedData = array();
foreach ($fullData as $key => $value) {
$slicedData[$key] = array($value[$tournamentIdx]);
}

$this->assertEquals($expData, $slicedData);

$this->cache_json_api_output('loadTournaments', 'noargs', $retval);
}

/*
* verify_api_loadTournamentData() - helper routine which calls the API
* loadTournamentData method, makes standard assertions about its
Expand Down
Loading