Skip to content

Commit 59ac67d

Browse files
committed
initial launch
1 parent dcfb868 commit 59ac67d

37 files changed

+2026
-0
lines changed

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
__pycache__
2+
build/
3+
dist/
4+
*.egg-info/
5+
*.pypirc
6+
*.pyc

draceditor/__init__.py

Whitespace-only changes.

draceditor/admin.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from django.contrib import admin
2+
from django.db import models
3+
4+
from .widgets import AdminMarkdownxWidget
5+
from .models import MarkdownxField
6+
7+
8+
class MarkdownxModelAdmin(admin.ModelAdmin):
9+
10+
formfield_overrides = {
11+
MarkdownxField: {'widget': AdminMarkdownxWidget}
12+
}

draceditor/api.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import json
2+
import base64
3+
import requests
4+
from .settings import (IMGUR_CLIENT_ID, IMGUR_API_KEY)
5+
6+
requests.packages.urllib3.disable_warnings()
7+
8+
9+
def imgur_uploader(image):
10+
"""
11+
Basic imgur uploader return as json data.
12+
:param `image` is from `request.FILES['markdown-image-upload']`
13+
14+
Return:
15+
success: {'status': 200, 'link': <link_image>, 'name': <image_name>}
16+
error : {'status': <error_code>, 'erorr': <erorr_message>}
17+
"""
18+
url_api = 'https://api.imgur.com/3/upload.json'
19+
headers = {'Authorization': 'Client-ID ' + IMGUR_CLIENT_ID}
20+
response = requests.post(
21+
url_api,
22+
headers=headers,
23+
data={
24+
'key': IMGUR_API_KEY,
25+
'image': base64.b64encode(image.read()),
26+
'type': 'base64',
27+
'name': image.name
28+
}
29+
)
30+
31+
"""
32+
Some function we got from `response`:
33+
34+
['connection', 'content', 'cookies', 'elapsed', 'encoding', 'headers','history',
35+
'is_permanent_redirect', 'is_redirect', 'iter_content', 'iter_lines', 'json',
36+
'links', 'ok', 'raise_for_status', 'raw', 'reason', 'request', 'status_code', 'text', 'url']
37+
"""
38+
if response.status_code == 200:
39+
respdata = json.loads(response.content.decode('utf-8'))
40+
return json.dumps({
41+
'status': respdata['status'],
42+
'link': respdata['data']['link'],
43+
'name': respdata['data']['name']
44+
})
45+
elif response.status_code == 415:
46+
# Unsupport File type
47+
return json.dumps({
48+
'status': response.status_code,
49+
'error': response.reason
50+
})
51+
return json.dumps({
52+
'status': response.status_code,
53+
'error': response.content.decode('utf-8')
54+
})

draceditor/extensions/__init__.py

Whitespace-only changes.

draceditor/extensions/emoji.py

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import markdown
2+
from ..settings import DRACEDITOR_MARKDOWN_BASE_EMOJI_URL
3+
4+
"""
5+
>>> import markdown
6+
>>> md = markdown.Markdown(extensions=['draceditor.utils.extensions.emoji'])
7+
>>> md.convert(':smile:')
8+
'<p><img class="direct-emoji-img" src="https://assets-cdn.github.com/images/icons/emoji/smile.png" /></p>'
9+
>>>
10+
"""
11+
12+
EMOJI_RE = r'(:[+\-\w]+:)'
13+
EMOJIS = [':bowtie:', ':smile:', ':laughing:', ':blush:', ':smiley:', ':relaxed:', ':smirk:', ':heart_eyes:',
14+
':kissing_heart:', ':kissing_closed_eyes:', ':flushed:', ':relieved:', ':satisfied:', ':grin:', ':wink:',
15+
':stuck_out_tongue_winking_eye:', ':stuck_out_tongue_closed_eyes:', ':grinning:', ':kissing:', ':kissing_smiling_eyes:',
16+
':stuck_out_tongue:', ':sleeping:', ':worried:', ':frowning:', ':anguished:', ':open_mouth:', ':grimacing:', ':confused:',
17+
':hushed:', ':expressionless:', ':unamused:', ':sweat_smile:', ':sweat:', ':disappointed_relieved:', ':weary:', ':pensive:',
18+
':disappointed:', ':confounded:', ':fearful:', ':cold_sweat:', ':persevere:', ':cry:', ':sob:', ':joy:', ':astonished:',
19+
':scream:', ':neckbeard:', ':tired_face:', ':angry:', ':rage:', ':triumph:', ':sleepy:', ':yum:', ':mask:', ':sunglasses:',
20+
':dizzy_face:', ':imp:', ':smiling_imp:', ':neutral_face:', ':no_mouth:', ':innocent:', ':alien:', ':yellow_heart:',
21+
':blue_heart:', ':purple_heart:', ':heart:', ':green_heart:', ':broken_heart:', ':heartbeat:', ':heartpulse:', ':two_hearts:',
22+
':revolving_hearts:', ':cupid:', ':sparkling_heart:', ':sparkles:', ':star:', ':star2:', ':dizzy:', ':boom:', ':collision:',
23+
':anger:', ':exclamation:', ':question:', ':grey_exclamation:', ':grey_question:', ':zzz:', ':dash:', ':sweat_drops:',
24+
':notes:', ':musical_note:', ':fire:', ':hankey:', ':poop:', ':shit:', ':thumbsup:', ':-1:', ':thumbsdown:', ':ok_hand:',
25+
':punch:', ':facepunch:', ':fist:', ':v:', ':wave:', ':hand:', ':raised_hand:', ':open_hands:', ':point_up:', ':point_down:',
26+
':point_left:', ':point_right:', ':raised_hands:', ':pray:', ':point_up_2:', ':clap:', ':muscle:', ':metal:', ':fu:', ':runner:',
27+
':running:', ':couple:', ':family:', ':two_men_holding_hands:', ':two_women_holding_hands:', ':dancer:', ':dancers:', ':ok_woman:',
28+
':no_good:', ':information_desk_person:', ':raising_hand:', ':bride_with_veil:', ':person_with_pouting_face:', ':person_frowning:',
29+
':bow:', ':couplekiss:', ':couple_with_heart:', ':massage:', ':haircut:', ':nail_care:', ':boy:', ':girl:', ':woman:', ':man:',
30+
':baby:', ':older_woman:', ':older_man:', ':person_with_blond_hair:', ':man_with_gua_pi_mao:', ':man_with_turban:', ':construction_worker:',
31+
':cop:', ':angel:', ':princess:', ':smiley_cat:', ':smile_cat:', ':heart_eyes_cat:', ':kissing_cat:', ':smirk_cat:', ':scream_cat:',
32+
':crying_cat_face:', ':joy_cat:', ':pouting_cat:', ':japanese_ogre:', ':japanese_goblin:', ':see_no_evil:', ':hear_no_evil:',
33+
':speak_no_evil:', ':guardsman:', ':skull:', ':feet:', ':lips:', ':kiss:', ':droplet:', ':ear:', ':eyes:', ':nose:', ':tongue:',
34+
':love_letter:', ':bust_in_silhouette:', ':busts_in_silhouette:', ':speech_balloon:', ':thought_balloon:', ':feelsgood:', ':finnadie:',
35+
':goberserk:', ':godmode:', ':hurtrealbad:', ':rage1:', ':rage2:', ':rage3:', ':rage4:', ':suspect:', ':trollface:', ':sunny:',
36+
':umbrella:', ':cloud:', ':snowflake:', ':snowman:', ':zap:', ':cyclone:', ':foggy:', ':ocean:', ':cat:', ':dog:', ':mouse:',
37+
':hamster:', ':rabbit:', ':wolf:', ':frog:', ':tiger:', ':koala:', ':bear:', ':pig:', ':pig_nose:', ':cow:', ':boar:', ':monkey_face:',
38+
':monkey:', ':horse:', ':racehorse:', ':camel:', ':sheep:', ':elephant:', ':panda_face:', ':snake:', ':bird:', ':baby_chick:',
39+
':hatched_chick:', ':hatching_chick:', ':chicken:', ':penguin:', ':turtle:', ':bug:', ':honeybee:', ':ant:', ':beetle:', ':snail:',
40+
':octopus:', ':tropical_fish:', ':fish:', ':whale:', ':whale2:', ':dolphin:', ':cow2:', ':ram:', ':rat:', ':water_buffalo:', ':tiger2:',
41+
':rabbit2:', ':dragon:', ':goat:', ':rooster:', ':dog2:', ':pig2:', ':mouse2:', ':ox:', ':dragon_face:', ':blowfish:', ':crocodile:',
42+
':dromedary_camel:', ':leopard:', ':cat2:', ':poodle:', ':paw_prints:', ':bouquet:', ':cherry_blossom:', ':tulip:', ':four_leaf_clover:',
43+
':rose:', ':sunflower:', ':hibiscus:', ':maple_leaf:', ':leaves:', ':fallen_leaf:', ':herb:', ':mushroom:', ':cactus:', ':palm_tree:',
44+
':evergreen_tree:', ':deciduous_tree:', ':chestnut:', ':seedling:', ':blossom:', ':ear_of_rice:', ':shell:', ':globe_with_meridians:',
45+
':sun_with_face:', ':full_moon_with_face:', ':new_moon_with_face:', ':new_moon:', ':waxing_crescent_moon:', ':first_quarter_moon:',
46+
':waxing_gibbous_moon:', ':full_moon:', ':waning_gibbous_moon:', ':last_quarter_moon:', ':waning_crescent_moon:',
47+
':last_quarter_moon_with_face:', ':first_quarter_moon_with_face:', ':crescent_moon:', ':earth_africa:', ':earth_americas:',
48+
':earth_asia:', ':volcano:', ':milky_way:', ':partly_sunny:', ':octocat:', ':squirrel:', ':bamboo:', ':gift_heart:', ':dolls:',
49+
':school_satchel:', ':mortar_board:', ':flags:', ':fireworks:', ':sparkler:', ':wind_chime:', ':rice_scene:', ':jack_o_lantern:',
50+
':ghost:', ':santa:', ':christmas_tree:', ':gift:', ':bell:', ':no_bell:', ':tanabata_tree:', ':tada:', ':confetti_ball:',
51+
':balloon:', ':crystal_ball:', ':cd:', ':dvd:', ':floppy_disk:', ':camera:', ':video_camera:', ':movie_camera:', ':computer:',
52+
':tv:', ':iphone:', ':phone:', ':telephone:', ':telephone_receiver:', ':pager:', ':fax:', ':minidisc:', ':vhs:', ':sound:',
53+
':speaker:', ':mute:', ':loudspeaker:', ':mega:', ':hourglass:', ':hourglass_flowing_sand:', ':alarm_clock:', ':watch:',
54+
':radio:', ':satellite:', ':loop:', ':mag:', ':mag_right:', ':unlock:', ':lock:', ':lock_with_ink_pen:', ':closed_lock_with_key:',
55+
':key:', ':bulb:', ':flashlight:', ':high_brightness:', ':low_brightness:', ':electric_plug:', ':battery:', ':calling:', ':email:',
56+
':mailbox:', ':postbox:', ':bath:', ':bathtub:', ':shower:', ':toilet:', ':wrench:', ':nut_and_bolt:', ':hammer:', ':seat:',
57+
':moneybag:', ':yen:', ':dollar:', ':pound:', ':euro:', ':credit_card:', ':money_with_wings:', ':e-mail:', ':inbox_tray:',
58+
':outbox_tray:', ':envelope:', ':incoming_envelope:', ':postal_horn:', ':mailbox_closed:', ':mailbox_with_mail:',
59+
':mailbox_with_no_mail:', ':package:', ':door:', ':smoking:', ':bomb:', ':gun:', ':hocho:', ':pill:', ':syringe:', ':page_facing_up:',
60+
':page_with_curl:', ':bookmark_tabs:', ':bar_chart:', ':chart_with_upwards_trend:', ':chart_with_downwards_trend:', ':scroll:',
61+
':clipboard:', ':calendar:', ':date:', ':card_index:', ':file_folder:', ':open_file_folder:', ':scissors:', ':pushpin:', ':paperclip:',
62+
':black_nib:', ':pencil2:', ':straight_ruler:', ':triangular_ruler:', ':closed_book:', ':green_book:', ':blue_book:',
63+
':orange_book:', ':notebook:', ':notebook_with_decorative_cover:', ':ledger:', ':books:', ':bookmark:', ':name_badge:',
64+
':microscope:', ':telescope:', ':newspaper:', ':football:', ':basketball:', ':soccer:', ':baseball:', ':tennis:',
65+
':8ball:', ':rugby_football:', ':bowling:', ':golf:', ':mountain_bicyclist:', ':bicyclist:', ':horse_racing:', ':snowboarder:',
66+
':swimmer:', ':surfer:', ':ski:', ':spades:', ':hearts:', ':clubs:', ':diamonds:', ':gem:', ':ring:', ':trophy:',
67+
':musical_score:', ':musical_keyboard:', ':violin:', ':space_invader:', ':video_game:', ':black_joker:', ':flower_playing_cards:',
68+
':game_die:', ':dart:', ':mahjong:', ':clapper:', ':memo:', ':pencil:', ':book:', ':art:', ':microphone:', ':headphones:',
69+
':trumpet:', ':saxophone:', ':guitar:', ':shoe:', ':sandal:', ':high_heel:', ':lipstick:', ':boot:', ':shirt:', ':tshirt:',
70+
':necktie:', ':womans_clothes:', ':dress:', ':running_shirt_with_sash:', ':jeans:', ':kimono:', ':bikini:', ':ribbon:',
71+
':tophat:', ':crown:', ':womans_hat:', ':mans_shoe:', ':closed_umbrella:', ':briefcase:', ':handbag:', ':pouch:', ':purse:',
72+
':eyeglasses:', ':fishing_pole_and_fish:', ':coffee:', ':tea:', ':sake:', ':baby_bottle:', ':beer:', ':beers:', ':cocktail:',
73+
':tropical_drink:', ':wine_glass:', ':fork_and_knife:', ':pizza:', ':hamburger:', ':fries:', ':poultry_leg:', ':meat_on_bone:',
74+
':spaghetti:', ':curry:', ':fried_shrimp:', ':bento:', ':sushi:', ':fish_cake:', ':rice_ball:', ':rice_cracker:', ':rice:',
75+
':ramen:', ':stew:', ':oden:', ':dango:', ':egg:', ':bread:', ':doughnut:', ':custard:', ':icecream:', ':ice_cream:',
76+
':shaved_ice:', ':birthday:', ':cake:', ':cookie:', ':chocolate_bar:', ':candy:', ':lollipop:', ':honey_pot:', ':apple:',
77+
':green_apple:', ':tangerine:', ':lemon:', ':cherries:', ':grapes:', ':watermelon:', ':strawberry:', ':peach:', ':melon:',
78+
':banana:', ':pear:', ':pineapple:', ':sweet_potato:', ':eggplant:', ':tomato:', ':corn:', ':house:', ':house_with_garden:',
79+
':school:', ':office:', ':post_office:', ':hospital:', ':bank:', ':convenience_store:', ':love_hotel:', ':hotel:', ':wedding:',
80+
':church:', ':department_store:', ':european_post_office:', ':city_sunrise:', ':city_sunset:', ':japanese_castle:',
81+
':european_castle:', ':tent:', ':factory:', ':tokyo_tower:', ':japan:', ':mount_fuji:', ':sunrise_over_mountains:', ':sunrise:',
82+
':stars:', ':statue_of_liberty:', ':bridge_at_night:', ':carousel_horse:', ':rainbow:', ':ferris_wheel:', ':fountain:',
83+
':roller_coaster:', ':ship:', ':speedboat:', ':boat:', ':sailboat:', ':rowboat:', ':anchor:', ':rocket:', ':airplane:',
84+
':helicopter:', ':steam_locomotive:', ':tram:', ':mountain_railway:', ':bike:', ':aerial_tramway:', ':suspension_railway:',
85+
':mountain_cableway:', ':tractor:', ':blue_car:', ':oncoming_automobile:', ':car:', ':red_car:', ':taxi:', ':oncoming_taxi:',
86+
':articulated_lorry:', ':bus:', ':oncoming_bus:', ':rotating_light:', ':police_car:', ':oncoming_police_car:', ':fire_engine:',
87+
':ambulance:', ':minibus:', ':truck:', ':train:', ':station:', ':train2:', ':bullettrain_front:', ':bullettrain_side:',
88+
':light_rail:', ':monorail:', ':railway_car:', ':trolleybus:', ':ticket:', ':fuelpump:', ':vertical_traffic_light:',
89+
':traffic_light:', ':warning:', ':construction:', ':beginner:', ':atm:', ':slot_machine:', ':busstop:', ':barber:',
90+
':hotsprings:', ':checkered_flag:', ':crossed_flags:', ':izakaya_lantern:', ':moyai:', ':circus_tent:', ':performing_arts:',
91+
':round_pushpin:', ':triangular_flag_on_post:', ':jp:', ':kr:', ':cn:', ':us:', ':fr:', ':es:', ':it:', ':ru:', ':gb:', ':uk:',
92+
':de:', ':one:', ':two:', ':three:', ':four:', ':five:', ':six:', ':seven:', ':eight:', ':nine:', ':keycap_ten:', ':1234:',
93+
':zero:', ':hash:', ':symbols:', ':arrow_backward:', ':arrow_down:', ':arrow_forward:', ':arrow_left:', ':capital_abcd:',
94+
':abcd:', ':abc:', ':arrow_lower_left:', ':arrow_lower_right:', ':arrow_right:', ':arrow_up:', ':arrow_upper_left:',
95+
':arrow_upper_right:', ':arrow_double_down:', ':arrow_double_up:', ':arrow_down_small:', ':arrow_heading_down:', ':arrow_heading_up:',
96+
':leftwards_arrow_with_hook:', ':arrow_right_hook:', ':left_right_arrow:', ':arrow_up_down:', ':arrow_up_small:',
97+
':arrows_clockwise:', ':arrows_counterclockwise:', ':rewind:', ':fast_forward:', ':information_source:', ':ok:',
98+
':twisted_rightwards_arrows:', ':repeat:', ':repeat_one:', ':new:', ':top:', ':up:', ':cool:', ':free:', ':ng:', ':cinema:',
99+
':koko:', ':signal_strength:', ':u5272:', ':u5408:', ':u55b6:', ':u6307:', ':u6708:', ':u6709:', ':u6e80:', ':u7121:',
100+
':u7533:', ':u7a7a:', ':u7981:', ':sa:', ':restroom:', ':mens:', ':womens:', ':baby_symbol:', ':no_smoking:', ':parking:',
101+
':wheelchair:', ':metro:', ':baggage_claim:', ':accept:', ':wc:', ':potable_water:', ':put_litter_in_its_place:', ':secret:',
102+
':congratulations:', ':m:', ':passport_control:', ':left_luggage:', ':customs:', ':ideograph_advantage:', ':cl:', ':sos:',
103+
':id:', ':no_entry_sign:', ':underage:', ':no_mobile_phones:', ':do_not_litter:', ':non-potable_water:', ':no_bicycles:',
104+
':no_pedestrians:', ':children_crossing:', ':no_entry:', ':eight_spoked_asterisk:', ':sparkle:', ':eight_pointed_black_star:',
105+
':heart_decoration:', ':vs:', ':vibration_mode:', ':mobile_phone_off:', ':chart:', ':currency_exchange:', ':aries:', ':taurus:',
106+
':gemini:', ':cancer:', ':leo:', ':virgo:', ':libra:', ':scorpius:', ':sagittarius:', ':capricorn:', ':aquarius:', ':pisces:',
107+
':ophiuchus:', ':six_pointed_star:', ':negative_squared_cross_mark:', ':a:', ':b:', ':ab:', ':o2:', ':diamond_shape_with_a_dot_inside:',
108+
':recycle:', ':end:', ':back:', ':on:', ':soon:', ':clock1:', ':clock130:', ':clock10:', ':clock1030:', ':clock11:', ':clock1130:',
109+
':clock12:', ':clock1230:', ':clock2:', ':clock230:', ':clock3:', ':clock330:', ':clock4:', ':clock430:', ':clock5:', ':clock530:',
110+
':clock6:', ':clock630:', ':clock7:', ':clock730:', ':clock8:', ':clock830:', ':clock9:', ':clock930:', ':heavy_dollar_sign:',
111+
':copyright:', ':registered:', ':tm:', ':x:', ':heavy_exclamation_mark:', ':bangbang:', ':interrobang:', ':o:', ':heavy_multiplication_x:',
112+
':heavy_plus_sign:', ':heavy_minus_sign:', ':heavy_division_sign:', ':white_flower:', ':100:', ':heavy_check_mark:',
113+
':ballot_box_with_check:', ':radio_button:', ':link:', ':curly_loop:', ':wavy_dash:', ':part_alternation_mark:', ':trident:',
114+
':black_small_square:', ':white_small_square:', ':black_medium_small_square:', ':white_medium_small_square:', ':black_medium_square:',
115+
':white_medium_square:', ':white_large_square:', ':white_check_mark:', ':black_square_button:', ':white_square_button:',
116+
':black_circle:', ':white_circle:', ':red_circle:', ':large_blue_circle:', ':large_blue_diamond:', ':large_orange_diamond:',
117+
':small_blue_diamond:', ':small_orange_diamond:', ':small_red_triangle:', ':small_red_triangle_down:', ':shipit:']
118+
119+
120+
class EmojiPattern(markdown.inlinepatterns.Pattern):
121+
122+
def handleMatch(self, m):
123+
emoji = self.unescape(m.group(2))
124+
if emoji not in EMOJIS:
125+
return emoji
126+
url = '{0}{1}.png'.format(
127+
DRACEDITOR_MARKDOWN_BASE_EMOJI_URL, emoji.replace(':', '')
128+
)
129+
el = markdown.util.etree.Element('img')
130+
el.set('src', url)
131+
el.set('class', 'direct-emoji-img')
132+
el.text = markdown.util.AtomicString(emoji)
133+
return el
134+
135+
136+
class EmojiExtension(markdown.Extension):
137+
138+
def extendMarkdown(self, md, md_globals):
139+
""" Setup `emoji_img` with EmojiPattern """
140+
md.inlinePatterns['emoji_img'] = EmojiPattern(EMOJI_RE, md)
141+
142+
143+
def makeExtension(*args, **kwargs):
144+
return EmojiExtension(*args, **kwargs)

draceditor/extensions/mention.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import markdown
2+
from django.contrib.auth.models import User
3+
from ..settings import (
4+
DRACEDITOR_MARKDOWN_BASE_MENTION_URL
5+
)
6+
7+
"""
8+
>>> import markdown
9+
>>> md = markdown.Markdown(extensions=['draceditor.utils.extensions.mention'])
10+
>>> md.convert('@[summonagus]')
11+
'<p><a href="https://forum.dracos-linux.org/profile/summonagus/">summonagus</a></p>'
12+
>>>
13+
>>> md.convert('hellp @[summonagus], i mentioned you!')
14+
'<p>hellp <a href="https://forum.dracos-linux.org/profile/summonagus/">summonagus</a>, i mentioned you!</p>'
15+
>>>
16+
"""
17+
18+
MENTION_RE = r'(?<!\!)\@\[([^\]]+)\]'
19+
20+
21+
class MentionPattern(markdown.inlinepatterns.Pattern):
22+
23+
def handleMatch(self, m):
24+
username = self.unescape(m.group(2))
25+
26+
"""Makesure `username` is registered."""
27+
if username in [u.username for u in User.objects.all() if u.is_active]:
28+
url = '{0}{1}/'.format(DRACEDITOR_MARKDOWN_BASE_MENTION_URL, username)
29+
el = markdown.util.etree.Element('a')
30+
el.set('href', url)
31+
el.set('class', 'direct-mention-link')
32+
el.text = markdown.util.AtomicString('@' + username)
33+
return el
34+
35+
36+
class MentionExtension(markdown.Extension):
37+
38+
def extendMarkdown(self, md, md_globals):
39+
""" Setup `mention_link` with MentionPattern """
40+
md.inlinePatterns['mention_link'] = MentionPattern(MENTION_RE, md)
41+
42+
43+
def makeExtension(*args, **kwargs):
44+
return MentionExtension(*args, **kwargs)

0 commit comments

Comments
 (0)