Răsfoiți Sursa

Update the word count modal (#27068)

GitOrigin-RevId: c4d11bda020e435bcf8b6daec253cedb37df0252
Alf Eaton 1 an în urmă
părinte
comite
cfcb9f32ab

+ 3 - 2
services/web/frontend/extracted-translations.json

@@ -248,6 +248,7 @@
   "change_your_email": "",
   "changing_the_position_of_your_figure": "",
   "changing_the_position_of_your_table": "",
+  "characters": "",
   "chat": "",
   "chat_error": "",
   "check_logs": "",
@@ -997,6 +998,7 @@
   "main_bibliography_file_for_this_project": "",
   "main_document": "",
   "main_file_not_found": "",
+  "main_text": "",
   "make_a_copy": "",
   "make_email_primary_description": "",
   "make_owner": "",
@@ -1767,7 +1769,6 @@
   "test_configuration_successful": "",
   "tex_live_version": "",
   "texgpt": "",
-  "text": "",
   "thank_you": "",
   "thank_you_exclamation": "",
   "thank_you_for_your_feedback": "",
@@ -1910,7 +1911,6 @@
   "tooltip_show_filetree": "",
   "tooltip_show_panel": "",
   "tooltip_show_pdf": "",
-  "total": "",
   "total_due_in_x_days": "",
   "total_due_today": "",
   "total_per_month": "",
@@ -2114,6 +2114,7 @@
   "with_premium_subscription_you_also_get": "",
   "word_count": "",
   "word_count_lower": "",
+  "words": "",
   "work_in_vim_or_emacs_emulation_mode": "",
   "work_offline": "",
   "work_offline_pull_to_overleaf": "",

+ 8 - 1
services/web/frontend/js/features/word-count-modal/components/word-count-modal-content.tsx

@@ -9,6 +9,7 @@ import OLButton from '@/shared/components/ol/ol-button'
 import { WordCountServer } from './word-count-server'
 import { WordCountClient } from './word-count-client'
 import { isSplitTestEnabled } from '@/utils/splitTestUtils'
+import SplitTestBadge from '@/shared/components/split-test-badge'
 
 // NOTE: this component is only mounted when the modal is open
 export default function WordCountModalContent({
@@ -21,7 +22,13 @@ export default function WordCountModalContent({
   return (
     <>
       <OLModalHeader closeButton>
-        <OLModalTitle>{t('word_count')}</OLModalTitle>
+        <OLModalTitle>
+          {t('word_count')}{' '}
+          <SplitTestBadge
+            splitTestName="word-count-client"
+            displayOnVariants={['enabled']}
+          />
+        </OLModalTitle>
       </OLModalHeader>
 
       <OLModalBody>

+ 69 - 46
services/web/frontend/js/features/word-count-modal/components/word-counts-client.tsx

@@ -5,6 +5,8 @@ import { Container, Row, Col, Form } from 'react-bootstrap'
 import OLNotification from '@/shared/components/ol/ol-notification'
 import usePersistedState from '@/shared/hooks/use-persisted-state'
 
+const numberFormat = new Intl.NumberFormat()
+
 export const WordCountsClient: FC<{ data: WordCountData }> = ({ data }) => {
   const { t } = useTranslation()
 
@@ -17,7 +19,7 @@ export const WordCountsClient: FC<{ data: WordCountData }> = ({ data }) => {
     return [
       {
         key: 'text',
-        label: t('text'),
+        label: t('main_text'),
         words: data.textWords,
         chars: data.textCharacters,
       },
@@ -85,52 +87,73 @@ export const WordCountsClient: FC<{ data: WordCountData }> = ({ data }) => {
         </Row>
       )}
 
-      {items.map(item => (
-        <Row
-          key={item.key}
-          style={{
-            borderBottom: '1px solid #eee',
-            padding: 5,
-            marginBottom: 5,
-          }}
-        >
-          <Col
-            style={{
-              display: 'flex',
-              alignItems: 'top',
-              justifyContent: 'space-between',
-            }}
-          >
-            <Form.Check
-              type="checkbox"
-              id={`word-count-${item.key}`}
-              label={item.label}
-              checked={included.includes(item.key)}
-              onChange={event =>
-                setIncluded(prevValue => {
-                  return event.target.checked
-                    ? prevValue.concat(item.key)
-                    : prevValue.filter(key => key !== item.key)
-                })
-              }
-              aria-label={`Include ${item.label} in total`}
-            />
-          </Col>
-          <Col>
-            {item.words} words
-            <br />
-            {item.chars} chars
-          </Col>
-        </Row>
-      ))}
+      <Row className="mb-4">
+        <table style={{ width: 'auto' }}>
+          <thead>
+            <tr>
+              <th />
+              <th className="visually-hidden">{t('words')}</th>
+              <th className="visually-hidden">{t('characters')}</th>
+            </tr>
+          </thead>
+          <tbody>
+            <tr>
+              <th style={{ width: 100 }}>{t('total_words')}:</th>
+              <td style={{ width: 100, textAlign: 'right' }}>
+                {numberFormat.format(totals.words)}
+              </td>
+              <td style={{ width: 250, textAlign: 'right' }}>
+                <b style={{ marginRight: 10 }} aria-hidden="true">
+                  {t('characters')}:
+                </b>{' '}
+                {numberFormat.format(totals.chars)}
+              </td>
+            </tr>
+            {items.map(item => (
+              <tr key={item.key}>
+                <th style={{ fontWeight: 'normal', paddingLeft: 20 }}>
+                  <Form.Check
+                    type="checkbox"
+                    id={`word-count-${item.key}`}
+                    label={`${item.label}:`}
+                    checked={included.includes(item.key)}
+                    onChange={event =>
+                      setIncluded(prevValue => {
+                        return event.target.checked
+                          ? prevValue.concat(item.key)
+                          : prevValue.filter(key => key !== item.key)
+                      })
+                    }
+                    aria-label={`Include ${item.label} in total`}
+                  />
+                </th>
+                <td style={{ textAlign: 'right' }}>
+                  {numberFormat.format(item.words)}
+                </td>
+                <td style={{ textAlign: 'right' }}>
+                  {numberFormat.format(item.chars)}
+                </td>
+              </tr>
+            ))}
+          </tbody>
+        </table>
+      </Row>
+
+      <Row className="border-top py-2">
+        <Col xs={12}>
+          <b>Headers:</b> {data.headers}
+        </Col>
+      </Row>
+
+      <Row className="border-top py-2">
+        <Col xs={12}>
+          <b>Math Inline:</b> {data.mathInline}
+        </Col>
+      </Row>
 
-      <Row>
-        <Col style={{ textAlign: 'right' }}>
-          <span style={{ fontWeight: 'bold' }}>
-            {t('total')}: {totals.words} words
-            <br />
-            {totals.chars} chars
-          </span>
+      <Row className="border-top py-2 pb-0">
+        <Col xs={12}>
+          <b>Math Display:</b> {data.mathDisplay}
         </Col>
       </Row>
     </Container>

+ 9 - 9
services/web/frontend/js/features/word-count-modal/utils/count-words-in-file.ts

@@ -5,7 +5,7 @@ import { debugConsole } from '@/utils/debugging'
 import { findPreambleExtent } from '@/features/word-count-modal/utils/find-preamble-extent'
 import { Segmenters } from './segmenters'
 
-const whiteSpaceRe = /^\s$/
+// const whiteSpaceRe = /^\s$/
 
 type Context = 'text' | 'header' | 'abstract' | 'caption' | 'footnote' | 'other'
 
@@ -301,9 +301,8 @@ export const countWordsInFile = (
   for (const [context, text] of Object.entries(texts)) {
     const counter = counters[context as Context]
 
-    // TODO: replace - and _ with a word character if hyphenated words should be counted as one word?
-
     for (const value of segmenters.word.segment(
+      // replace - and _ with a word character, so that hyphenated words are counted as one word
       text.replace(/\w[-_]\w/g, 'aaa')
     )) {
       if (value.isWordLike) {
@@ -311,13 +310,14 @@ export const countWordsInFile = (
       }
     }
 
-    // TODO: count hyphens as characters?
-
-    for (const value of segmenters.character.segment(text)) {
+    for (const _value of segmenters.character.segment(
+      // replace multiple spaces with a single space
+      text.replace(/\s+/, ' ').trim()
+    )) {
       // TODO: option for whether to include whitespace?
-      if (!whiteSpaceRe.test(value.segment)) {
-        data[counter.character]++
-      }
+      // if (!whiteSpaceRe.test(value.segment)) {
+      data[counter.character]++
+      // }
     }
   }
 }

+ 3 - 0
services/web/locales/en.json

@@ -317,6 +317,7 @@
   "change_your_email": "Change your email",
   "changing_the_position_of_your_figure": "Changing the position of your figure",
   "changing_the_position_of_your_table": "Changing the position of your table",
+  "characters": "Characters",
   "chat": "Chat",
   "chat_error": "Could not load chat messages, please try again.",
   "check_logs": "Check logs",
@@ -1307,6 +1308,7 @@
   "main_bibliography_file_for_this_project": "Main bibliography file for this project",
   "main_document": "Main document",
   "main_file_not_found": "Unknown main document",
+  "main_text": "Main text",
   "maintenance": "Maintenance",
   "make_a_copy": "Make a copy",
   "make_email_primary_description": "Make this the primary email, used to log in",
@@ -2670,6 +2672,7 @@
   "with_premium_subscription_you_also_get": "With an Overleaf Premium subscription you also get",
   "word_count": "Word Count",
   "word_count_lower": "Word count",
+  "words": "Words",
   "work_in_vim_or_emacs_emulation_mode": "Work in Vim or Emacs emulation mode",
   "work_offline": "Work offline",
   "work_offline_pull_to_overleaf": "Work offline, then pull to __appName__",