Skip to content

Commit

Permalink
Merge 23847c4 into 0513414
Browse files Browse the repository at this point in the history
  • Loading branch information
rodfersou committed Jul 7, 2016
2 parents 0513414 + 23847c4 commit d023fd4
Show file tree
Hide file tree
Showing 10 changed files with 119 additions and 4 deletions.
4 changes: 4 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ Changelog
1.0b2 (unreleased)
------------------

- Review text extraction. Now we have a field in the controlpanel to manage a blacklist
of css classes.
[rodfersou]

- To avoid displaying the 'Listen' button with an incorrect voice,
the feature is now globally disabled by default at installation time.
[hvelarde]
Expand Down
15 changes: 15 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,18 @@ A viewlet with a 'Listen' button will be displayed on objects with the feature e
The Text-To-Speech feature enabled.

You can pause/resume the reader at any time by selecting 'Pause'/'Resume'.

How does it work
----------------

Text to speech extract the text of the page, and send the text to `ResponsiveVoice <http://responsivevoice.org/>`_ to play the text.

By default, the text extraction need to ignore some elements that should not be played:

* Blockquotes
* IFrames
* Image captions

And there are other custom elements that should be ignored, to manage these custom elements we use a blacklist.

The blacklist is filled into the package configlet and should be filled with one CSS selector by line that should be ignored.
2 changes: 2 additions & 0 deletions src/collective/texttospeech/browser/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,7 @@ def enabled(self):
interface=ITextToSpeechControlPanel, name='globally_enabled')
except InvalidParameterError:
globally_enabled = False
except KeyError:
globally_enabled = False

return globally_enabled
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
data-label-paused="Resume"
data-error-message="Could not load ResponsiveVoice library; Text-To-Speech feature is disabled or is not available."
tal:attributes="data-voice view/voice;
data-enabled view/enabled"
data-enabled view/enabled;
data-blacklist view/blacklist"
i18n:attributes="data-label-stopped;
data-label-playing;
data-label-paused;
Expand Down
18 changes: 18 additions & 0 deletions src/collective/texttospeech/browser/viewlets.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"""Viewlets used on the package."""
from collective.texttospeech.interfaces import ITextToSpeechControlPanel
from plone import api
from plone.api.exc import InvalidParameterError
from plone.app.layout.viewlets.common import ViewletBase


Expand All @@ -27,3 +28,20 @@ def voice(self):
return api.portal.get_registry_record(
ITextToSpeechControlPanel.__identifier__ + '.voice'
)

def blacklist(self):
record = dict(interface=ITextToSpeechControlPanel, name='css_class_blacklist')
try:
css_class_blacklist = api.portal.get_registry_record(**record)
if not css_class_blacklist:
css_class_blacklist = ()
classes = [c for c in css_class_blacklist]
# ignore blockquotes
classes.append('pullquote')
# ignore image captions
classes.append('image-caption')
except InvalidParameterError:
return ''
except KeyError:
return ''
return ','.join(classes)
12 changes: 12 additions & 0 deletions src/collective/texttospeech/interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,15 @@ class ITextToSpeechControlPanel(form.Schema):
required=True,
default=u'UK English Female',
)

form.widget('css_class_blacklist', cols=25, rows=10)
css_class_blacklist = schema.Set(
title=_(u'CSS class blacklist'),
description=_(
u'A list of CSS class identifiers that Text-to-Speech will ignore. '
u'elements with "pullquote" or "image-caption" class directly applied to them, will be skipped.'
),
required=False,
default=set([]),
value_type=schema.ASCIILine(title=_(u'CSS class')),
)
55 changes: 53 additions & 2 deletions src/collective/texttospeech/static/main.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
var MainView = (function() {
function MainView() {
this.$el = $('#viewlet-texttospeech');
this.$el.data('texttospeech', this);
this.$button = $('#texttospeech-button', this.$el);
this.$button.fadeIn();
this.voice = this.$el.attr('data-voice');
this.label_stopped = this.$el.attr('data-label-stopped');
this.label_playing = this.$el.attr('data-label-playing');
this.label_paused = this.$el.attr('data-label-paused');
this.blacklist = this.$el.attr('data-blacklist').split(',');
this.playing = false;
this.paused = true;
this.$button.on('click', $.proxy(this.play_pause, this));
Expand All @@ -23,6 +25,56 @@ var MainView = (function() {
this.$button.html(this.label_stopped);
this.$button.attr('class', 'stopped');
};
MainView.prototype.is_invisible = function($el) {
return $el.is(':visible') === false;
};
MainView.prototype.is_blacklisted = function($el) {
var i, len, selector;
var ignore = false;
for (i = 0, len = this.blacklist.length; i < len; i++) {
selector = '.' + this.blacklist[i];
if ($el.is(selector) || $el.parents(selector).length > 0) {
ignore = true;
break;
}
}
return ignore;
};
MainView.prototype.has_ending_punctuation = function(text) {
// regex test if the text end with one of these punctuations ". , ; : ! ? -"
return /[.,;:!?—]$/.test(text);
};
MainView.prototype.remove_extra_spaces = function(text) {
text = text.replace(/\s+/g, ' ');
text = text.trim();
return text
};
MainView.prototype.extract_text = function() {
var i, len, ref, results, $el, text;
// create an array with the text extracted
results = [];
// http://stackoverflow.com/questions/4602431/what-is-the-most-efficient-way-to-get-leaf-nodes-with-jquery
ref = $('#content *:not(:has(*))');
for (i = 0, len = ref.length; i < len; i++) {
$el = $(ref[i]);
if (this.is_invisible($el) || this.is_blacklisted($el)) {
continue;
}
text = $el.text();
text = this.remove_extra_spaces(text);
// ignore empty lines
if (text.length === 0) {
continue;
}
// ensure there is a pause after every line adding a period
if (!this.has_ending_punctuation(text)) {
text += '.';
}
results.push(text);
}
// join array with texts
return results.join(' ');
};
MainView.prototype.play_pause = function(e) {
e.preventDefault();
if (this.playing) {
Expand All @@ -39,8 +91,7 @@ var MainView = (function() {
}
} else {
responsiveVoice.speak(
// remove spaces to avoid issues with some Firefox versions
$('#content').text().replace(/\s+/g, ' ').trim(),
this.extract_text(),
this.voice, {
onstart: $.proxy(this.onstart, this),
onend: $.proxy(this.onend, this)
Expand Down
5 changes: 5 additions & 0 deletions src/collective/texttospeech/tests/test_controlpanel.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ def test_voice_record_in_registry(self):
self.assertTrue(hasattr(self.settings, 'voice'))
self.assertEqual(self.settings.voice, u'UK English Female')

def test_css_class_blacklist_record_in_registry(self):
self.assertTrue(hasattr(self.settings, 'css_class_blacklist'))
self.assertEqual(self.settings.css_class_blacklist, set([]))

def test_records_removed_on_uninstall(self):
qi = self.portal['portal_quickinstaller']
qi.uninstallProducts(products=[PROJECTNAME])
Expand All @@ -74,6 +78,7 @@ def test_records_removed_on_uninstall(self):
ITextToSpeechControlPanel.__identifier__ + '.globally_enabled',
ITextToSpeechControlPanel.__identifier__ + '.enabled_content_types',
ITextToSpeechControlPanel.__identifier__ + '.voice',
ITextToSpeechControlPanel.__identifier__ + '.css_class_blacklist',
]

for r in records:
Expand Down
2 changes: 1 addition & 1 deletion src/collective/texttospeech/tests/test_upgrades.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ def test_profile_version(self):

def test_registered_steps(self):
steps = len(self.setup.listUpgrades(self.profile_id)[0])
self.assertEqual(steps, 3)
self.assertEqual(steps, 4)

@unittest.skipIf(IS_PLONE_5, 'Upgrade step not supported under Plone 5')
def test_update_library_condition(self):
Expand Down
7 changes: 7 additions & 0 deletions src/collective/texttospeech/upgrades/v3/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@
handler="..cook_javascript_resources"
/>

<genericsetup:upgradeDepends
title="Add new field to configlet"
description="Reload registration of configlet registry to add new field."
import_steps="plone.app.registry"
run_deps="false"
/>

</genericsetup:upgradeSteps>

</configure>

0 comments on commit d023fd4

Please sign in to comment.