diff --git a/.gitignore b/.gitignore
index 54954c0..4e6d578 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,3 @@
/target
-/pkg/opentally.js
-/pkg/opentally_bg.wasm
+/html/opentally.js
+/html/opentally_bg.wasm
diff --git a/build_wasm.sh b/build_wasm.sh
index 50be928..d4d9df6 100755
--- a/build_wasm.sh
+++ b/build_wasm.sh
@@ -1,2 +1,2 @@
#!/bin/sh
-cargo build --lib --target wasm32-unknown-unknown && /home/runassudo/.cargo/bin/wasm-bindgen --target no-modules target/wasm32-unknown-unknown/debug/opentally.wasm --out-dir pkg --no-typescript
+cargo build --lib --target wasm32-unknown-unknown && /home/runassudo/.cargo/bin/wasm-bindgen --target no-modules target/wasm32-unknown-unknown/debug/opentally.wasm --out-dir html --no-typescript
diff --git a/html/index.html b/html/index.html
new file mode 100644
index 0000000..f3b6f40
--- /dev/null
+++ b/html/index.html
@@ -0,0 +1,250 @@
+
+
+
+
+
+ OpenTally
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Printing directly from this page is not supported. Use the ‘Print result’ button to generate a printer-friendly report.
+
+
+
+
+
+
+
diff --git a/html/index.js b/html/index.js
new file mode 100644
index 0000000..da2c09b
--- /dev/null
+++ b/html/index.js
@@ -0,0 +1,77 @@
+/* OpenTally: Open-source election vote counting
+ * Copyright © 2021 Lee Yingtong Li (RunasSudo)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+function clickAdvancedOptions() {
+ if (document.getElementById('divAdvancedOptions').style.display === 'none') {
+ document.getElementById('divAdvancedOptions').style.display = 'grid';
+ document.getElementById('btnAdvancedOptions').innerHTML = 'Hide advanced options';
+ } else {
+ document.getElementById('divAdvancedOptions').style.display = 'none';
+ document.getElementById('btnAdvancedOptions').innerHTML = 'Show advanced options';
+ }
+}
+
+console.log = function(v) {
+ document.getElementById('resultLogs1').append(v);
+ document.getElementById('resultLogs1').append("\n");
+};
+
+var wasm = wasm_bindgen;
+
+async function clickCount() {
+ if (document.getElementById('bltFile').files.length === 0) {
+ return;
+ }
+
+ // Read BLT file
+ let bltFile = document.getElementById('bltFile').files[0];
+ let electionData = await bltFile.text();
+
+ // Load WASM
+ await wasm_bindgen('opentally_bg.wasm');
+
+ // Init election
+ let election = wasm.election_from_blt_Rational(electionData);
+ let state = wasm.CountStateRational.new(election);
+
+ // Init STV options
+ let stv_opts = wasm.STVOptions.new(
+ document.getElementById('chkRoundTVs').checked ? parseInt(document.getElementById('txtRoundTVs').value) : null,
+ document.getElementById('chkRoundWeights').checked ? parseInt(document.getElementById('txtRoundWeights').value) : null,
+ document.getElementById('chkRoundVotes').checked ? parseInt(document.getElementById('txtRoundVotes').value) : null,
+ document.getElementById('chkRoundQuota').checked ? parseInt(document.getElementById('txtRoundQuota').value) : null,
+ document.getElementById('selQuota').value,
+ document.getElementById('selQuotaCriterion').value,
+ document.getElementById('selTransfers').value,
+ document.getElementById('selSurplus').value,
+ document.getElementById('selPapers').value == 'transferable',
+ document.getElementById('selExclusion').value,
+ parseInt(document.getElementById('txtPPDP').value),
+ );
+
+ // Step election
+ wasm.count_init_Rational(state, stv_opts);
+ wasm.make_and_print_result_Rational(1, state);
+
+ for (let stage_num = 2;; stage_num++) {
+ let is_done = wasm.count_one_stage_Rational(state, stv_opts);
+ if (is_done) {
+ break;
+ }
+ wasm.make_and_print_result_Rational(stage_num, state);
+ }
+}
diff --git a/html/main.css b/html/main.css
new file mode 100644
index 0000000..be1024f
--- /dev/null
+++ b/html/main.css
@@ -0,0 +1,257 @@
+/*
+ pyRCV2: Preferential vote counting
+ Copyright © 2020–2021 Lee Yingtong Li (RunasSudo)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
+@import url('https://fonts.googleapis.com/css2?family=Source+Sans+Pro&display=swap');
+
+html, body {
+ font-family: 'Source Sans Pro', sans-serif;
+}
+
+body {
+ padding: 0.5em;
+}
+
+a {
+ color: #1d46c4;
+ text-decoration: none;
+}
+a:hover {
+ color: #1d3da2;
+ text-decoration: underline;
+}
+
+/* Menu styling */
+
+.menudiv {
+ border-bottom: 1px solid #ccc;
+ line-height: 1.8;
+ margin-bottom: 0.5em;
+ padding-bottom: 0.5em;
+}
+
+.menudiv .subheading {
+ font-size: 0.8em;
+ font-weight: bold;
+}
+
+.cols-12 {
+ display: grid;
+ grid-template-columns: repeat(12, 1fr);
+}
+
+.col-3 { grid-column-end: span 3; }
+.col-6 { grid-column-end: span 6; }
+.col-12 { grid-column-end: span 12; }
+
+@media screen and (max-width: 17.5cm) {
+ .cols-sm-6 {
+ grid-template-columns: repeat(6, 1fr);
+ }
+}
+
+/* Count table */
+
+table {
+ border-collapse: collapse;
+}
+.result td {
+ padding: 0px 8px;
+ min-height: 1em;
+}
+td.count {
+ text-align: right;
+}
+td.count sup {
+ font-size: 0.6rem;
+ top: 0;
+}
+tr.stage-no td, tr.stage-kind td, tr.stage-comment td {
+ text-align: center;
+}
+tr.stage-no td:not(:first-child) {
+ border-top: 1px solid #76858c;
+}
+tr.stage-kind td:not(:first-child) {
+ font-size: 0.75em;
+ min-width: 5rem;
+ color: #1b2839;
+ background-color: #f0f5fb;
+ color-adjust: exact;
+ -webkit-print-color-adjust: exact;
+}
+td.excluded {
+ background-color: #fde2e2;
+ color-adjust: exact;
+ -webkit-print-color-adjust: exact;
+}
+td.elected {
+ background-color: #e0fdc5;
+ color-adjust: exact;
+ -webkit-print-color-adjust: exact;
+}
+tr.info td {
+ background-color: #f0f5fb;
+ color-adjust: exact;
+ -webkit-print-color-adjust: exact;
+}
+td.bt {
+ border-top: 1px solid #76858c;
+}
+td.bb {
+ border-bottom: 1px solid #76858c;
+}
+
+/* BLT input tool */
+
+#selBallots {
+ min-width: 10em;
+ margin-right: 1em;
+}
+
+#bltMain {
+ display: flex;
+}
+
+#tblBallot {
+ margin-top: 0.5em;
+ margin-bottom: 0.5em;
+}
+
+#tblBallot input {
+ margin-right: 0.5ex;
+}
+
+#divEditCandidates div {
+ margin-bottom: 0.5em;
+}
+
+#txtCandidates {
+ min-width: 20em;
+ min-height: 10em;
+}
+
+/* Print stylesheet */
+
+#printWarning {
+ display: none;
+}
+
+#printContainer > div:first-child > p:first-child {
+ margin-top: 0;
+}
+
+@media print {
+ body.interactive > * {
+ display: none;
+ }
+ #divAdvancedOptions, #printPane {
+ /* Override inline style */
+ display: none !important;
+ }
+ #printWarning {
+ display: block;
+ }
+}
+
+/* Form styling */
+/* Adapted in part from https://github.com/nathansmith/formalize (GPL/MIT) */
+
+select, input, button {
+ line-height: 1.15;
+}
+
+select, input[type="text"], input[type="number"], textarea {
+ appearance: none;
+ background-color: #fff;
+ border: 1px solid;
+ border-color: #999 #bbb #ddd;
+ border-radius: 0;
+ box-sizing: border-box;
+ color: #000;
+ padding: 2px 3px;
+}
+
+select {
+ /* Dropdown arrow */
+ background-image: url(data:image/png;base64,R0lGODlhDQAEAIAAAAAAAP8A/yH5BAEHAAEALAAAAAANAAQAAAILhA+hG5jMDpxvhgIAOw==);
+ background-position: right center;
+ background-repeat: no-repeat;
+ padding-right: 20px; /* Padding for dropdown arrow */
+}
+
+button, input[type="file"]::-webkit-file-upload-button {
+ background-color: #f0f0f0;
+ border: 1px solid;
+ border-color: #ddd #bbb #999;
+ border-radius: 4px;
+ color: #000;
+ font-family: inherit;
+ padding: 2px 10px;
+}
+button:hover, input[type="file"]::-webkit-file-upload-button:hover {
+ background-color: #eaeaea;
+}
+button:active, input[type="file"]::-webkit-file-upload-button:active {
+ background-color: #dfdfdf;
+ border-color: #999 #bbb #ddd;
+}
+
+/* Chrome can't parse this and ignores the entire rule */
+input[type="file"]::file-selector-button {
+ background-color: #f0f0f0;
+ border: 1px solid;
+ border-color: #ddd #bbb #999;
+ border-radius: 4px;
+ color: #000;
+ font-family: inherit;
+ padding: 2px 10px;
+}
+button:hover, input[type="file"]::file-selector-button:hover {
+ background-color: #f5f5f5;
+}
+button:active, input[type="file"]::file-selector-button:active {
+ background-color: #eaeaea;
+ border-color: #999 #bbb #ddd;
+}
+
+input[type="checkbox"] {
+ appearance: none;
+ position: relative;
+ width: 0.9em;
+ height: 0.9em;
+ border: 1px solid;
+ border-color: #999 #bbb #ddd;
+ vertical-align: -2px;
+}
+input[type="checkbox"]:checked {
+ background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABhGlDQ1BJQ0MgcHJvZmlsZQAAKJF9kT1Iw1AUhU9Ti0VaHMwg4pChOlkQFXHUKhShQqgVWnUweekfNGlJUlwcBdeCgz+LVQcXZ10dXAVB8AfEzc1J0UVKvC8ptIjxwuN9nHfP4b37AKFZYbrVMw7ohm2mkwkpm1uVel8RQAhhRCEqzKrNyXIKvvV1T91Ud3Ge5d/3Z0W1vMWAgEQ8y2qmTbxBPL1p1zjvE4uspGjE58RjJl2Q+JHrqsdvnIsuCzxTNDPpeWKRWCp2sdrFrGTqxFPEMU03KF/Ieqxx3uKsV+qsfU/+wkjeWFnmOq1hJLGIJciQoKKOMiqwEafdIMVCms4TPv4h1y+TSyVXGYwcC6hCh+L6wf/g92ytwuSElxRJAKEXx/kYAXp3gVbDcb6PHad1AgSfgSuj4682gZlP0hsdLXYE9G8DF9cdTd0DLneAwaeaYiquFKQlFArA+xl9Uw4YuAX61ry5tc9x+gBkaFapG+DgEBgtUva6z7vD3XP7t6c9vx/h6nJtOtJaHwAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAAOxAAADsQBlSsOGwAAAAd0SU1FB+UBBQgnA808S3QAAABpSURBVDjLxZNLEsAgCEPRO3pKuOPrrkXRDtZFs/PzkjGMBUAOVOVQnwzM7FmwKVVFRFBVAOpucmut39xN9ukAcgJ3BuNBBr4NxmKycDDwFzNweIIHMnAo0YMZeDqFVR9pg7eJzFR+/40Xt0cwTrDq4YsAAAAASUVORK5C);
+ background-position: center;
+ background-repeat: no-repeat;
+ background-size: contain;
+}
+
+button:focus, select:focus, input:focus, textarea:focus {
+ outline: 0;
+}
+select:focus, input:focus, textarea:focus {
+ border-color: #3daee9;
+}
+
+label {
+ white-space: nowrap;
+}
diff --git a/pkg/test.html b/pkg/test.html
deleted file mode 100644
index a0c579c..0000000
--- a/pkg/test.html
+++ /dev/null
@@ -1,49 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/src/numbers/rational_num.rs b/src/numbers/rational_num.rs
index 5013c00..5937109 100644
--- a/src/numbers/rational_num.rs
+++ b/src/numbers/rational_num.rs
@@ -247,9 +247,7 @@ impl ops::SubAssign<&Rational> for Rational {
}
impl ops::MulAssign<&Rational> for Rational {
- fn mul_assign(&mut self, _rhs: &Rational) {
- todo!()
- }
+ fn mul_assign(&mut self, rhs: &Rational) { self.0 *= &rhs.0 }
}
impl ops::DivAssign<&Rational> for Rational {
diff --git a/src/stv/wasm.rs b/src/stv/wasm.rs
index 2a075dc..7f7b2dd 100644
--- a/src/stv/wasm.rs
+++ b/src/stv/wasm.rs
@@ -154,22 +154,3 @@ fn print_stage(stage_num: usize, result: &StageResult) {
cprintln!("");
}
-
-/*
-#[wasm_bindgen]
-pub struct STVOptions(stv::STVOptions);
-#[wasm_bindgen]
-impl STVOptions {
- pub fn new(round_votes: Option, exclusion: String) -> Self {
- if exclusion == "one_round" {
- return STVOptions(stv::STVOptions {
- round_votes: round_votes,
- exclusion: &"one_round",
- pp_decimals: 2,
- });
- } else {
- panic!("Unknown --exclusion");
- }
- }
-}
-*/